🧃

Docker Buildxのcacheまとめ

2022/11/23に公開

概要

個人の備忘録としてDocker Buildxとdocker buildxでビルドを行う際のキャッシュについてまとめていきます。

Docker Buildx

Docker BuildxはDockerのCLIプラグインで、BuildKitを利用して従来のdokcer buildの機能を拡張しています。

https://github.com/docker/buildx

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

https://github.com/moby/buildkit

詳細
https://blog.mobyproject.org/introducing-buildkit-17e056cc5317

proposal
https://github.com/moby/moby/issues/32925

BuildKit

レガシーなビルダーに取って代わった新しいバックエンドで、ビルドのパフォーマンスやDockerfileの再利用性を高めるためのアドバンスドな機能が備わっています。加えて、次のようなより複雑なシナリオに対するハンドリングの機能がサポートされています。

  • 使用されていなビルドステージの検知とスキップ
  • 独立したビルドステージのパラレルビルド
  • ビルドコンテキストで変更されたファイルのみのインクリメンタルな移行
  • buildコンテキストで使用されてないファイルの検知と移行のスキップ
  • たくさんん機能によるDockerfileのフロントエンド実装の利用
  • REST APIの副作用の回避(中間イメージやコンテナなど)
  • 自動削除のためのビルドキャッシュの優先

加えて、多くの機能とは別にBuildKitはパフォーマンス、ストレージ管理そして拡張性が改善されてるそうです。
パフォーマンス面では、並列のビルドグラフ。最終生成物に影響を与えずアウトコマンドの最適化が可能な場合にビルドステップを並列実行します。加えてローカルソースファイルへのアクセスも最適化します。繰り返しのビルド実行時に、ソースファイルの変更された部分のみをトラッキングすることで、ビルドが開始する前にローカルファイルが読み込まれたりアップロードされたりするのを待つ必要がなくなります。

https://docs.docker.com/build/buildkit/

LLB

BuildKitのコアはLLB(Low-Level Build)の定義フォーマットです。
LLBは中間バイナリフォーマットで開発者がBuildKitを拡張するために使われます。
LLBはコンテンツのアドレス可能な依存関係グラフを定義します。これは非常に複雑なビルド定義をまとめるために使用されます。また、Dockerfileの機能で提供されてないような、データマウンティングやネスト実行などの内部機能のために利用されたりもしてます。

ビルドの実行とキャッシュの全てがLLBにおいて定義されています。
キャッシュモデルはレガシーなビルダーから完全に書き直されています。
ヒューリスティックを使用してイメージを比較するのではなく、LLBは直接ビルドグラフのチェックサムと特定の操作に対するコンテンツをトラックしています。これによりより早く、正確に、そしてポータブルになりました。

Frontend

frontendはhuman readableなビルドフォーマットをLLBに変換するためのコンポーネントです。
frotendはイメージとして配布可能で、ターゲットバージョンの指定もできます。

https://docs.docker.com/build/buildkit/dockerfile-frontend/

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デーモンに直接接続します

https://docs.docker.com/build/building/drivers/

それぞれのユースケースに応じて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 .

https://docs.docker.com/build/building/drivers/docker/

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

https://docs.docker.com/build/building/drivers/docker-container/

cache管理によるDockerビルドの最適化

ここからはDockerのビルドをキャッシュを使った最適化についてみていきます。

参考
https://docs.docker.com/build/building/cache/

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 --from=fetch /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のビルド環境では必須になってきます。

https://docs.docker.com/build/building/cache/backends/

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がキャッシュされます。
minmodeはキャッシュサイズが小さい一方、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について見ていきます。

参考
https://docs.docker.com/build/ci/github-actions/

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を追加する必要があります。下記で記載されているように古いキャッシュが自動では削除されないためです。

https://github.com/docker/build-push-action/issues/252

https://github.com/moby/buildkit/issues/1896

関連:
https://zenn.dev/sasakiki/articles/5b9974ce2a72b3

Discussion