Docker Buildxのcacheまとめ
概要
個人の備忘録としてDocker Buildxとdocker buildxでビルドを行う際のキャッシュについてまとめていきます。
Docker Buildx
Docker BuildxはDockerのCLIプラグインで、BuildKitを利用して従来のdokcer buildの機能を拡張しています。
docker buildx buildコマンドはdocker command
と同じ使い方でスコープ化されたbuilder instanceを作成したり、複数のnodeを並列にビルドしたり、configurationの出力、インラインのbuild caching、ターゲットプラットフォームを特定する機能などのようなたくさんの機能をサポートしています。
加えて、Buildxは通常のdocker build
コマンドでは提供されていないマニフェストリストのビルド、分散キャッシュ、そしてOCI image tarballsへのビルド結果の出力などの機能もサポートしています。
BuildKit自身は次のような特徴を持っています。
- Automatic garbage collection
- Extendable frontend formats
- Concurrent dependency resolution
- Efficient instruction caching
- Build cache import/export
- Nested build job invocations
- Distributable workers
- Multiple output formats
- Pluggable architecture
- Execution without root privileges
詳細
proposal
BuildKit
レガシーなビルダーに取って代わった新しいバックエンドで、ビルドのパフォーマンスやDockerfileの再利用性を高めるためのアドバンスドな機能が備わっています。加えて、次のようなより複雑なシナリオに対するハンドリングの機能がサポートされています。
- 使用されていなビルドステージの検知とスキップ
- 独立したビルドステージのパラレルビルド
- ビルドコンテキストで変更されたファイルのみのインクリメンタルな移行
- buildコンテキストで使用されてないファイルの検知と移行のスキップ
- たくさんん機能によるDockerfileのフロントエンド実装の利用
- REST APIの副作用の回避(中間イメージやコンテナなど)
- 自動削除のためのビルドキャッシュの優先
加えて、多くの機能とは別にBuildKitはパフォーマンス、ストレージ管理そして拡張性が改善されてるそうです。
パフォーマンス面では、並列のビルドグラフ。最終生成物に影響を与えずアウトコマンドの最適化が可能な場合にビルドステップを並列実行します。加えてローカルソースファイルへのアクセスも最適化します。繰り返しのビルド実行時に、ソースファイルの変更された部分のみをトラッキングすることで、ビルドが開始する前にローカルファイルが読み込まれたりアップロードされたりするのを待つ必要がなくなります。
LLB
BuildKitのコアはLLB(Low-Level Build)の定義フォーマットです。
LLBは中間バイナリフォーマットで開発者がBuildKitを拡張するために使われます。
LLBはコンテンツのアドレス可能な依存関係グラフを定義します。これは非常に複雑なビルド定義をまとめるために使用されます。また、Dockerfileの機能で提供されてないような、データマウンティングやネスト実行などの内部機能のために利用されたりもしてます。
ビルドの実行とキャッシュの全てがLLBにおいて定義されています。
キャッシュモデルはレガシーなビルダーから完全に書き直されています。
ヒューリスティックを使用してイメージを比較するのではなく、LLBは直接ビルドグラフのチェックサムと特定の操作に対するコンテンツをトラックしています。これによりより早く、正確に、そしてポータブルになりました。
Frontend
frontendはhuman readableなビルドフォーマットをLLBに変換するためのコンポーネントです。
frotendはイメージとして配布可能で、ターゲットバージョンの指定もできます。
Buildx Driver
続いてBuildxに戻ってBuildx Driverについて見ていきます。
Buildx DriverはBuildkitのバックエンドがどこでどのように実行するかを定めた設定です。
Buildxは次のdriverをサポートしています
-
docker
:Docker daemonにバンドルされたBuildKitライブラリを使用します -
docker-container
:Dockerを利用してdedicated BuildKiktコンテナを作成します -
kubernetes
:Kubernetesクラスター上にBuildKitのPodを作成します -
remote
: 手動で管理されたBuildKitデーモンに直接接続します
それぞれのユースケースに応じてdriverを選択します。デフォルトのdocker
ドライバーはシンプルで簡易な場合に利用します。dockerドライバーはキャッシュやフォーマットの出力のようなアドバンスド機能が制限されていて、設定することができません。
利用可能なビルダーの一覧
コマンドdocker buildx ls
を利用してシステム上で利用可能なビルダーインスタンスとそれが利用するdriverを一覧表示できます。
% docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
desktop-linux docker
desktop-linux desktop-linux running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default * docker
default default running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
新規ビルダーの作成
コマンドdocker buildx create
コマンドでbuilderを作成することができます。
オプション--driver
でdriverを指定できます。
docker buildx create --name=<builder-name> --driver=<driver> --driver-opt=<driver-options>
Docker driver
Buildx Dockerドライバーはデフォルトドライバーで、BuildKitサーバーコンポーネントのビルドをDockerエンジンに直接使用しています。Dockerドライバーではconfigurationが必要ありません。
docker buildx build .
Docker container driver
Buildxのdocker container driverはマネージドでカスタマイズ可能なBuildKit環境をdockerコンテナに作成することができます。
docker container driverはデフォルトのdocker driverに対して次の利点があります
- カスタムBuildKitバージョンの指定が可能
- マルチアーキテクチャイメージの利用が可能(参考:QEMU)
- キャッシュのインポート・エクスポート用のアドバンスドオプションが利用可能
次のコマンドはdocker container driverを利用してcontainer
という名前のビルダーを新規作成します。
docker buildx create \
--name container \
--driver=docker-container \
--driver-opt=[key=value,...]
container
cache管理によるDockerビルドの最適化
ここからはDockerのビルドをキャッシュを使った最適化についてみていきます。
参考
Dockerのビルドで時間を改善する際に最も重要なことはDockerのビルドキャッシュを利用することです。
FROM ubuntu:latest
RUN apt-get update && apt-get install -y build-essentials
COPY main.c Makefile /src/
WORKDIR /src/
RUN make build
Dockerfile中のそれぞれの行のタスクはDockerイメージの中で次のようにlayer上に構成されます。
image layerをstackとして捉えることができ、それぞれのlayerはそれよりも前のlayerに対して積み上げていくように構成されます。
layerが変更した場合(例えば プログラムmain.c
を変更した場合)、イメージにこの変更が適用されるようにCOPY
コマンドは再度実行される必要があります。つまり、DockerはこのCOPY
コマンドのlayerのキャッシュは無効化する必要があります。
あるlayerが変更された場合、それ以降のlayerにも影響があり再度実行する必要があります。
キャッシュの効率化
1:キャッシュの順番
重たいステップをDockerfileの先頭の方に、よく変更が入るステップをDockerfileの後ろの方に配置することでキャッシュの最適化を図ります。
FROM node
WORKDIR /app
COPY . . # Copy over all files in the current directory
RUN npm install # Install dependencies
RUN npm build # Run build
上の例の場合だと、プログラムに変更が入るたびにそれ以降のSTEPがキャッシュが効かず、npm install
が実行されてしまい時間がかかってしまいます。
FROM node
WORKDIR /app
COPY package.json yarn.lock . # Copy package management files
RUN npm install # Install dependencies
COPY . . # Copy over project files
RUN npm build # Run build
そのため、上の例のようにCOPYステップを二つのSTEPに分けます。パッケージマネージメントファイルのCOPYをはじめに行って続けてnpm install
を行い、その後COPY . .
でそれ以降のファイルをコピーするようにします。これでファイルに変更があった再もパッケージ管理ファイルに変更がなければnpm install
までの実行はキャッシュが効くようになります。
2:layerを小さく保つ
実行する処理をできるだけ少なくすることで、変更が入る余地を小さくして、rebuildが発生する可能性を小さくすることでキャッシュによりビルド時間の短縮を狙います。
次のように必要なファイルのみをCOPYするように指定します。
COPY ./src ./Makefile /src
↓NG
COPY ../src
または.dockerignoreファイルを利用して除外するファイルを指定することもできます。
3:layer数を最小に保つ
layer数を最小に保つことでDockerビルドに必要な時間を減らすことができます。
そのための方法として
適切なbase imageを使用
ビルド対象のアプリケーションに適切なdockerのbase imageを利用することでビルド時間の短縮とイメージを最新に保ってセキュアにすることができます。
マルチステージビルドの利用
マルチステージビルドはDockerfileのステップを複数のステージに分割することができます。
それぞれのステージはビルドステップで完了し、それぞれのステージをブリッジすることで最終イメージを作ることができます。
例
# stage 1
FROM alpine as git
RUN apk add git
# stage 2
FROM git as fetch
WORKDIR /repo
RUN git clone https://github.com/your/repository.git .
# stage 3
FROM nginx as site
COPY /repo/docs/ /usr/share/nginx/html
各ステージで必要なもののみを含めることでイメージサイズを縮小できそしてセキュアにできます。
可能な限りコマンドを組み合わせる
次のように二つのStepにRUNコマンドを分けるのではなく、
RUN echo "the first command"
RUN echo "the second command"
次のように&&
を使ってRUNの実行を1つにまとめてしまいます。一つにまとめることで同じキャッシュにすることができます。
RUN echo "the first command" && echo "the second command"
# or to split to multiple lines
RUN echo "the first command" && \
echo "the second command"
cache storage backend
BuildKitはビルドを高速化するために、自動でinternalキャッシュにビルド結果をキャッシュします。
加えて、BuildKitは外部へのキャッシュのexportもサポートしていて、未来のビルド時にキャッシュを利用できるようにしています。
外部のキャッシュはCI/CDのビルド環境では必須になってきます。
Buildxは次のストレージバックエンドをサポートしています。
-
inline
: docker imageにビルドキャッシュを埋め込みます -
registry
:分離したイメージにビルドキャッシュをpushします。 -
local
:ファイルシステム上のローカルディレクトリにビルドキャッシュを書き込みます -
gha
:GitHub Action cacheにビルドキャッシュをアップロードします(beta) -
s3
:AWS S3バケットにビルドキャッシュをアップロードします(未リリース) -
azblob
:Azure Blob Storageにビルドキャッシュをアップロードします(未リリース)
コマンドSyntax
docker buildx build --push -t <registry>/<image> \
--cache-to type=registry,ref=<registry>/<cache-image>[,parameters...] \
--cache-from type=registry,ref=<registry>/<cache-image>[,parameters...] .
--cache-toオプションでキャッシュ保存先であるストレージのバックエンドを指定します。
そして--cache-fromオプションでストレージバックエンドから現在のビルドにキャッシュをインポートするように指定します。
設定オプション
Cache mode
キャッシュを出力する際に、オプション--cache-to
は引数mode
オプションを指定でき、これによりどのlayerのキャッシュをexportするかを指定することができます。
modeはmode=min
またはmode=max
のどちらかを指定できます。
min
モードではイメージ結果に含まれるlayerのみがキャッシュされるのに対し、max
モードでは中間生成結果を含む全てのlayerがキャッシュされます。
min
modeはキャッシュサイズが小さい一方、max
モードはよりキャッシュヒット率が高くなります。
ビルドの複雑性や位置に応じて、それぞれのmodeのパラメータを比較してどちらがよく作用するかを検証するようにします。
Local cache
local
キャッシュはシンプルなキャッシュオプションで、ファイルシステム上のディレクトリにファイルとしてキャッシュを格納します。格納するディレクトリにOCI image layoutを利用します。
docker buildx build --push -t <registry>/<image> \
--cache-to type=local,dest=path/to/local/dir[,parameters...] \
--cache-from type=local,src=path/to/local/dir .
GitHub Actionsを使ったDockerイメージのCI/CD構築
ここで最後にGitHub Actionsを利用したDockerイメージのCI/CDについて見ていきます。
参考
name: ci
on:
push:
branches:
- "main"
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: user/app:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
-
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
docker-setup-buildxActionでActionsのworkflowでBuildxを使ったbuildを行えるbuilderをセットアップできます。driverにはBuildx用にdocker-containerドライバが利用されています。
「Cache Docker layers」ステップで、後続のステップでDockerイメージをビルドする際に作成したcacheがあればそのcacheからDockerイメージをリストアします。
docker-loginactionを利用してDockerレジストリへのログインを行い、
docker/build-push-actionでDockerのbuildとpushを行います。
最後に注意点として、GitHub Actionsを使ってDocker Buildxでビルドを行う場合、
ビルド前のcacheを削除して新規作成したcacheを次のビルド用のパスに移動してあげるようにstepを追加する必要があります。下記で記載されているように古いキャッシュが自動では削除されないためです。
関連:
Discussion