まとめ
BuildKit 使ってなくても COMPOSE_DOCKER_CLI_BUILD=1
を使う
キャッシュが効いてない
CI をいじっていてタイトルの問題に気づいたのが発端。
Cloud Build でキャッシュを使いつつ、ビルドとテストを実行するためにこうしていた。
- 1 前回のビルドのアプリケーションイメージを
docker pull
する - 2
docker build --cache-from
でキャッシュを利用しつつイメージを作る - 3
docker-compose run
で他のミドルウェア等を起動しつつイメージのテストを実行 - 4 テストが通れば 2 で作ったイメージを push
うまくいくと思っていたけど、2 と 3 で都度アプリケーションイメージがビルドされてしまう。なんでだろなあ、と調べていた。
試していると、COPY
を機に docker build
と docker-compose build
間でキャッシュが共有できなくなっていることに気づく。今まで違和感を覚えるタイミングは何回かあったけどスルーしていたなあ。
https://gist.github.com/pokutuna/e181f8e9e20a49e7cc6c46c75bd07a82
docker-compose build # Building app # Step 1/5 : FROM alpine:3.12.0 # ---> a24bb4013296 # Step 2/5 : RUN mkdir /app # ---> Using cache # ---> 2fe579ed6f0f # Step 3/5 : COPY print.sh /app # ---> a787e9b6de30
原因
Issue が 2015 年からある歴史の長い問題。
- docker-compose build and docker build lead to different IDs · Issue #883 · docker/compose
Client.build
anddocker build
don't share a build cache · Issue #998 · docker/docker-py
docker はファイルを tar で固めて転送し、ハッシュを比較してキャッシュを使えるか判定するんだけど、docker CLI(go) と docker-compose の利用する docker-py の間で生成される tar に差異があってキャッシュが利用できなくなる。tar ヘッダ部分の扱いに由来するようだ。
- archive/tar: remove file type bits from mode field (I3be7d946) · Gerrit Code Review
- go の archive/tar が tar の仕様にはないファイルタイプの bit を書かないようにする修正
- docker はコンテンツを比較する際にこの bit も考慮していた(意図的じゃなくて単にバイト列として比較していただけだろうけど)
- Clear uname/gname when creating tar archive by Larsjep · Pull Request #1582 · docker/docker-py
- docker-py で docker 側の実装に合わせてファイルの user, group をクリアする実装
- これで直るかと思いきや...
- pkg/archive.FileInfoHeader: fill file type bits by AkihiroSuda · Pull Request #33935 · moby/moby
- go 側の修正によりイメージの互換性が無くなるので docker 側でファイルタイプを入れる修正が入る
という流れ。2020-08-17 現在まだ解決はしていない。
Issue でのやりとりを見るに、世間が BuildKit に移行することでどうせキャッシュが切れるので、昔からあったこの問題を今修正して世の中のキャッシュを2回壊すより BuildKit 化による1回を選ぶ、という感じかな。BuildKit ではコンテンツハッシュに tar ヘッダを利用しないようになっている。
Workaround その1: COMPOSE_DOCKER_CLI_BUILD=1
じゃあどうするねんという話だけど、COMPOSE_DOCKER_CLI_BUILD=1
を指定するとよい。
これはよく "docker-compose で BuildKit を有効にする環境変数" と説明されたりするけど、少しずれていて docker-compose でのビルドを docker CLI に移譲するフラグである。移譲された docker CLI は DOCKER_BUILDKIT=1
により BuildKit を利用する。
BuildKit を使っていなくても、このフラグを使うことで docker CLI と docker-py の差異によるキャッシュ問題を回避できる。
周辺のコードを読んでいたら、docker-compose run
を契機にビルドが走る際に COMPOSE_DOCKER_CLI_BUILD
が考慮されていなかったのを見つけたので PR を投げてマージされた(えっへん)。これがリリースされたら run 時にビルドが走った場合にも BuildKit が使えるしキャッシュも効くようになる。今は1度 docker-compose build
する必要がある。
Use docker cli on docker-compose run when COMPOSE_DOCKER_CLI_BUILD=1 passed. by pokutuna · Pull Request #7653 · docker/compose
Workaround その2: stage 間で COPY する
別の回避策として、ステージ間の COPY
にする技があるらしい。
例えば上記の再現コードだと
FROM scratch as scratch COPY . /context FROM alpine:3.12.0 as app RUN mkdir /app COPY --from=scratch /context/print.sh /app ENV PATH /app:$PATH ENTRYPOINT ["print.sh"]
のように、1度なにもしないステージを経由してコピーする。 無駄のように思えるけど、ステージ間のコピーにすることで docker-py による問題から逃れられるというテク。 はじめの COPY 部分のキャッシュは効かないけど、ファイルの転送だけなのでほぼ時間はかからず、キャッシュの恩恵に預かれる。
よかったですね。
え、みんなもう BuildKit 使ってるから関係ない? そうですか......