skaffold のメモ

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-nginxmy-php-fpm が Pod や Deployment の image と一致していると、該当箇所が skaffold によってビルドされたタグ付きのイメージに置換されます。

例えば次の Pod のマニュフェストだと image: my-nginximage: 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 で指定できる、とありますが紛らわしいことにここで指定したものは実際にマニュフェストが書き換えられるときのタグ値とは異なります。

参考: https://skaffold.dev/docs/taggers/

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 Renderer だと Helm チャートのファイル監視が効かないようで、チャートやテンプレートを修正しても再デプロイされません(後者だとチャートを編集すると自動的に再デプロイされる)。

イメージの削除

普通に skaffold dev していると Docker にビルドされたイメージがごみのようにたまります。skaffold を次のように実行しておくと Ctrl+C で止めたときに skaffold が作成していたイメージが削除されるようになります。

skaffold dev --no-prune=false --cache-artifacts=false

参考: https://skaffold.dev/docs/cleanup/

--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

参考: https://skaffold.dev/docs/references/cli/#skaffold-dev

ただ、これを設定すると当たり前だけれども 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 を叩いてデプロイを実行する

参考: https://skaffold.dev/docs/design/api/#control-api

マルチステージビルドの FileSync

ドキュメントにさらっと書いていますが、マルチステージビルドではどのステージを target にしているかにかかわらず、最後のステージのみが検査されます。ので、最後のステージ以外で COPY している場合は次のような manual の同期ルールが必要です。

参考: https://skaffold.dev/docs/filesync/#manual-sync-mode

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 で良いかな。