EKS/Kubernetes での開発のために skaffold を使ってみたメモ。 自分用メモなので How To 的なものはありません。
例
例のための最小限の skaffold.yaml
の構成。
apiVersion: skaffold/v3 kind: Config build: artifacts: - image: my-app docker: dockerfile: Dockerfile sync: infer: - app/** hooks: after: - container: command: ["sh", "-c", "kill -USR2 1 || true"] manifests: rawYaml: - manifests/*.yaml deploy: kubectl: {} portForward: - resourceType: service resourceName: my-app port: 80 localPort: 9876
これで skaffold dev
すると次のことが行われます。
- Dockerfile からイメージをビルド
my-app:<hash>
のようにタグ付けされる
- 元のマニュフェスト manifests/*.yaml から一部が置換された新しいマニュフェストを生成
- image がビルドでタグ付けされたイメージに置換されたり
- metadata.labels が追記されたり
- 生成したマニュフェストを
kubectl apply
でクラスタにデプロイ - ローカルの 9876 ポートを kubernetes の service/my-app/80 にポートフォワード
また、ファイルシステムが監視され、ファイルの編集に応じて次のように処理されます。
- Dockerfile、または、Dockerfile で COPY しているファイル(app/** は除く)
- ビルド~マニュフェスト生成~デプロイ、のやり直し
- app/**
- 変更されたファイルをコンテナにコピー(
kubectl cp
) - コンテナの pid:1 に USR2 シグナルを送る
- 変更されたファイルをコンテナにコピー(
- manifests/*.yaml
- マニュフェストを再生成してクラスタにデプロイ
マニュフェストのイメージの書き換え
ビルドされたイメージは skaffold によってタグ付けされ、元のマニュフェストファイルの image の箇所に置換したうえでデプロイされます。
例えば次の skaffold.yaml の場合、
# skaffold.yaml apiVersion: skaffold/v3 kind: Config build: artifacts: - image: my-nginx docker: dockerfile: docker/nginx/Dockerfile - image: my-php-fpm docker: dockerfile: docker/php-fpm/Dockerfile manifests: rawYaml: - manifests/*.yaml
build.artifacts.*.image
に指定した my-nginx
や my-php-fpm
が Pod や Deployment の image と一致していると、該当箇所が skaffold によってビルドされたタグ付きのイメージに置換されます。
例えば次の Pod のマニュフェストだと image: my-nginx
や image: my-php-fpm
は一致するビルドイメージがあるので置換されます。image: my-nodejs
は一致するものが無いので置換されません。
# manifests/pod.yaml apiVersion: v1 kind: Pod metadata: name: my-app spec: containers: - name: nginx image: my-nginx - name: php-fpm image: my-php-fpm - name: nodejs image: my-nodejs
次のコマンドで置換されたマニュフェストの内容が表示できます。
skaffold build -q | skaffold render -a -
skaffold がタグをどのように生成するかは skaffold.yaml の build.tagPolicy
で指定できる、とありますが紛らわしいことにここで指定したものは実際にマニュフェストが書き換えられるときのタグ値とは異なります。
build.tagPolicy
はイメージのビルド時に付けられるタグを決めるもので、イメージのビルド前に計算されます。(ローカルクラスタの場合)ビルド後、ビルドされたイメージの ID そのものでタグがさらに追加され、マニュフェストファイルはその ID そのものであるタグ値で書き換えられます。
例)build.tagPolicy
に基づくタグ(デフォルトは git のコミットハッシュ)と IMAGE ID そのもののタグの両方が保存される
REPOSITORY TAG IMAGE ID my-nginx 3c77f04 649cff01c83d my-nginx 649cff01c83d 649cff01c83d
なお、skaffold はマニュフェスト内から image というキーを探してきて置換します。ので、次のように image というラベルがあるとそれも置換されます。
# manifests/pod.yaml apiVersion: v1 kind: Pod metadata: name: my-app labels: image: my-nginx # ここも置換される(label として不正な文字が含まれることになるためデプロイでエラーとなる) spec: containers: - name: nginx image: my-nginx
これが問題になるようなことはまずないと思うけれども、次のように置換する位置は制限できます。
# skaffold.yaml resourceSelector: allow: - groupKind: "Pod" image: [".spec.containers.*.image"]
参考: https://skaffold.dev/docs/tutorials/skaffold-resource-selector/
Helm
skaffold の Helm のサポートは Helm Renderer(manifest.helm.*
)と Helm Deployer(deploy.helm.*
)の2種類存在します。
Helm Renderer
# skaffold.yaml apiVersion: skaffold/v3 kind: Config build: # ...snip... manifests: helm: releases: - name: my-app chartPath: helm/ setValueTemplates: image.fqn: '{{ .IMAGE_FULLY_QUALIFIED_my_app}}' upgradeOnChange: true
Helm Deployer
apiVersion: skaffold/v3 kind: Config build: # ...snip... deploy: helm: releases: - name: my-app chartPath: helm/ setValueTemplates: image.fqn: '{{ .IMAGE_FULLY_QUALIFIED_my_app}}' upgradeOnChange: true
これは次のような違いがあります。
- Helm Renderer
helm template
でマニュフェストを生成したうえでkubectl apply
でデプロイする
- Helm Deployer
- Helm チャートから直接
helm install
でデプロイする
- Helm チャートから直接
バグなのかどうかわからないけれども、Helm Renderer だと Helm チャートのファイル監視が効かないようで、チャートやテンプレートを修正しても再デプロイされません(後者だとチャートを編集すると自動的に再デプロイされる)。
イメージの削除
普通に skaffold dev
していると Docker にビルドされたイメージがごみのようにたまります。skaffold を次のように実行しておくと Ctrl+C で止めたときに skaffold が作成していたイメージが削除されるようになります。
skaffold dev --no-prune=false --cache-artifacts=false
--no-prune=false
は skaffold によってビルドされたイメージを終了時に削除するかどうかのオプションで、デフォルトだと prune されません。
--cache-artifacts=false
はアーティファクトのキャッシュを有効にするかどうかのフラグです。デフォルトでは ~/.skaffold/cacke
にアーティファクトのハッシュ(Dockerfile や COPY のソースなどのファイルのハッシュ)とイメージのダイジェスト(sha256)の対応が追記されています。イメージのビルド時、計算されたアーティファクトのハッシュに対応するイメージが存在するならビルドをスキップしてそのイメージが使われます。
参考: https://github.com/GoogleContainerTools/skaffold/issues/4842
skaffold の終了時に --no-prune=false
でイメージを削除する場合はアーティファクトキャッシュを有効にする意味が無いので --cache-artifacts=false
も一緒に指定するのが良いもよう。
都度コマンドラインオプションで指定するのも面倒なので環境変数でも指定できます。
export SKAFFOLD_NO_PRUNE=false export SKAFFOLD_CACHE_ARTIFACTS=false
ただ、これを設定すると当たり前だけれども skaffold dev
の開始のたびにフルビルドが走るため一長一短です。一日中 skaffold dev
しっぱなしならともかく、あげてはさげてを繰り返す場合、毎回フルビルドが走るのはつらい。
build.artifacts.*.docker.cacheFrom
などを活用してフルビルドを避けることも考えられそう。
Helm チャートを envsubst で生成
values.template.yaml から envsubst で values.yaml を生成していたので、skaffold でも values.template.yaml の編集で自動的に values.yaml を生成→デプロイしたかったのだけど、簡単にはできなさそう。
before-deploy フックで values.yaml を生成することを考えてみたけど、ダメ、無限ループに陥る。
- before-deploy フック → ファイル生成 → 変更を検出 → before-deploy フック → ファイル生成 → 変更を検出 → ...
参考: https://github.com/GoogleContainerTools/skaffold/issues/7183 \ 参考: https://github.com/GoogleContainerTools/skaffold/issues/7216
カスタムビルドで無理やりできなくもない?
apiVersion: skaffold/v3 kind: Config build: artifacts: - image: my-app-values custom: dependencies: paths: - helm/values.template.yaml buildCommand: | envsubst < helm/values.template.yaml > helm/values.yaml docker pull alpine docker tag alpine $IMAGE # ..snip..
values.template.yaml に変更があればこのビルドが走るので values.yaml が生成される。カスタムビルドは環境変数 $IMAGE で指定されたイメージをビルドしなければならないので(イメージをビルドするためのカスタムビルドなので)、適当なイメージを pull してタグ付けする。
これで出来なくもなさそうだけど・・適当な別ツールを併用するほうが素直で良さそう。
skaffold dev & watchexec -w helm/values.template.yaml -- 'envsubst < helm/values.template.yaml > helm/values.yaml' # values.template.yaml が編集されると values.yaml も更新される # values.yaml は skaffold が監視しているので自動デプロイが走る
あるいは自動デプロイを無効にしたうえで skaffold api
でデプロイをトリガできる。
skaffold dev --auto-deploy=false --rpc-http-port=50052 & watchexec -w helm/values.template.yaml -- ' envsubst < helm/values.template.yaml > helm/values.yaml &&\ curl -X POST http://localhost:50052/v1/execute -d "{\"deploy\": true}" ' # values.template.yaml が編集されて values.yaml が更新されたあと、 # skaffold の API を叩いてデプロイを実行する
マルチステージビルドの FileSync
ドキュメントにさらっと書いていますが、マルチステージビルドではどのステージを target にしているかにかかわらず、最後のステージのみが検査されます。ので、最後のステージ以外で COPY している場合は次のような manual の同期ルールが必要です。
Dockerfile をマルチステージビルドするように書き換えたところ skaffold の FileSync が動かなくなり、原因調査に小一時間ぐらい溶かしました。ドキュメントはしっかり読もう。
Deployment の minReadySeconds
k8s の Deployment で minReadySeconds に適当な値(60 秒とか)を指定したところ、skaffold dev
で環境を立ち上げた後、ポートフォワードが有効になるまでに 60 秒待たされるようになりました。Service でポートを公開しているポッドの Deployment とは別の、バックグラウンドジョブのための Deployment の minReadySeconds を指定しただけにも関わらずです。
どうも skaffold はマニュフェストで定義されているすべての Deployment が準備完了になるまでポートフォワードを開始しないようです。ので、実環境で minReadySeconds を設定する場合でも 、ローカル用のマニュフェストでは minReadySeconds が 0 になるようにしておくと良いです。
さいごに
実環境が Kubernetes でもローカルでの開発は docker compose を使っておく方が嵌りどころが少なくて良いと思います。ただ、なんらかの事情で Kubernetes を開発に使う必要があるならビルドからデプロイまでひとつひとつやるのはかなり煩雑なので skaffold でサクッと自動化すると便利です。
ただ skaffold dev
だけでいろいろされすぎて、裏で何がどうなっているのか理解しにくいようにも思いました。いっそのころ、どのファイルが編集されたときにどの処理が行われるかを明示的にすべて指定する、とかでも良いような気もしました。うーん、でもそれ、skaffold に対する grunt/gulp のようなもので、むしろ逆行してしまっているかも・・やっぱり skaffold で良いかな。