🤖

🤖

:gijutsu_burogu:

キャッシュのためにDockerビルドで中間イメージをタグ付けしレジストリにPushする

WHY: なぜ中間イメージをタグ付けするのか

今では Docker イメージの軽量化のためにマルチステージビルドは欠かせません。 ローカルでの実行では問題ありませんが、CI 環境などの dockerd が毎度変わる Docker In Docker(dind)では問題が起こります。

最終イメージの中にビルドプロセスの全てが含まれていれば 1 Dockerfile 1 Cache で単純な--cache-from を指定するだけで良いです。 マルチステージビルドは最終イメージの軽量化が目的であるため、最終イメージにキャッシュすべき情報はほとんど含まれておらずキャッシュしても無意味です。 キャッシュしたいステージを明示的に--targetで指定して、ビルドされたイメージを外部に保存する必要があります。

例えば、以下の Dockerfile を考えます。 stage1stage2は、キャッシュしたいです。

FROM alpine:latest as stage1
LABEL stage=1
WORKDIR /workdir
RUN sleep 3 && touch stage1.txt


FROM alpine:latest as stage2
LABEL stage=2
WORKDIR /workdir
RUN sleep 6 && touch stage2.txt

COPY --from=stage1 /workdir/stage1.txt /workdir/stage1.txt

FROM alpine:latest as stage3
LABEL stage=3
WORKDIR /workdir
COPY --from=stage1 /workdir/stage1.txt /workdir/stage1.txt
COPY --from=stage2 /workdir/stage2.txt /workdir/stage2.txt

HOW: どう中間イメージをタグ付けするのか

BuildKit を使ってビルドする場合とそうでない場合でやり方が異なります。 また、最終ステージにはCOPYのみのため、キャッシュを利用しません。

BuildKit なし

DockerfileLABELを使いラベル付けし、docker images--filterオプションを使うことタグ付したいイメージを取得できます。 LABEL stage=1のステージを取得するには、$(docker images --filter 'label=stage=1' -q | head -n 1)とすれば良いです。

export DOCKER_BUILDKIT=0

# キャッシュのためにPull
# &とwaitで並列実行
docker pull test:stage1 || true &
docker pull test:stage2 || true &
wait

# ビルドする
docker build --cache-from= .

# 中間イメージにタグ付けする
docker tag $(docker images --filter 'label=stage=1' -q | head -n 1) test:stage1
docker tag $(docker images --filter 'label=stage=2' -q | head -n 1) test:stage2

# キャッシュのためにPush
# &とwaitで並列実行
docker push test:latest &
docker push test:stage1 &
docker push test:stage2 &
wait
他の選択肢

このようにしてもできます。 キャッシュが効くのでタグ付けのためのビルドはすぐ終わるとはいえ、上のやり方の方が早いので良いです。

# ビルドする
docker build -t test:latest .

# 中間イメージにタグ付けする
docker build -t test:stage1 --target stage1 --cache-from test:stage1 .
docker build -t test:stage2 --target stage2 --cache-from test:stage2 .

BuildKit あり

最近では、BuildKit を使ってビルドするのが当たり前になっています。 BuildKit ではDockerfileをパースしステージ間の依存関係を解釈するためstage1stage2を並列にビルドでき、ビルド時間の短縮につながります。

しかし、BuildKit を使ったビルドでは中間イメージが生成されません。 docker imagesしても、最終イメージしか表示されません。 理由は分かりませんが、容量を圧迫しないようにあえてそうしているのでしょうか。

そのため、BuildKit なしの場合のように、docker tagによるタグ付けができません。 したがって、個別にビルドすることによってタグ付けします。

export DOCKER_BUILDKIT=1

# ビルドする
docker build -t test:latest --cache-from=test:stage1,test:stage2 --build-arg BUILDKIT_INLINE_CACHE=1  .

# 中間イメージにタグ付けする
# &とwaitで並列実行
docker build -t test:stage1 --target stage1 --cache-from test:stage1 --build-arg BUILDKIT_INLINE_CACHE=1  . &
docker build -t test:stage2 --target stage2 --cache-from test:stage2 --build-arg BUILDKIT_INLINE_CACHE=1  . &
wait

# キャッシュのためにPush
# &とwaitで並列実行
docker push test:latest &
docker push test:stage1 &
docker push test:stage2 &
wait

--cache-fromはまずレジストリからメタデータのみを取得し、キャッシュヒットの可能性があるレイヤーのみを Pull します。

docs.docker.com

BuildKit で最終ステージまでビルドしてから個別にステージをビルドすると一番無駄が少ないです。

ツールを使うことで簡単にビルドスクリプトを生成できます。 最終ステージをキャッシュするかどうかなど細かいチューニングは人の手で行ってください。

github.com

$ go get -u github.com/kotaroooo0/melancholy
$ melancholy -i image_name
# ----- Build image -----
docker build -t image_name:latest --cache-from=image_name:stage1,image_name:stage2,image_name:latest --build-arg BUILDKIT_INLINE_CACHE=1 .
# ----- Attach tags -----
docker build -t image_name:stage1 --target=stage1 --build-arg BUILDKIT_INLINE_CACHE=1 . &
docker build -t image_name:stage2 --target=stage2 --build-arg BUILDKIT_INLINE_CACHE=1 . &
wait
# ----- Push images -----
docker push image_name:stage1 &
docker push image_name:stage2 &
docker push image_name:latest &
wait