※ 本記事は WESEEK Tips wiki の記事 "/Tips/BuildKitによりDockerとDocker Composeで外部キャッシュを使った効率的なビルドをする方法" の転載です。
本記事を執筆したモチベーション
Docker Hub の Automated Build 機能を GitHub Actions へ移行する機会がありました。
移行をするだけであれば GitHub Actions で docker コンテナをビルドすればよいのですが、せっかくなので BuildKit を調べて使ってみることにしました。
本記事では BuildKit についての概要を記載し、Docker Hub にキャッシュを保存して効率よくビルドする方法について記載します。
また、Docker においてのみでなく Docker Compose においても BuildKit を使ってビルドする方法を紹介します。
※ BuildKit を使った Docker ビルドの目玉機能の一つであるセキュリティ向上に関する機能は実践していません。(時間があれば追記します)
BuildKit とは
BuildKit とはビルドツールキットです。
ソースコードを変換して効率的で表現力豊かで再現可能な方法で成果物を構築することが出来ます。
以下の特徴があります。(参考)
- 自動ガベージコレクション
- フロントエンドフォーマットの拡張
- 並列的な依存関係の解決
- 効率的な命令キャッシュ
- ビルドキャッシュのインポート/エクスポート
- ネストされたビルドジョブの呼び出し
- 分散ワーカー
- 複数のアウトプットフォーマット
- プラガブルなアーキテクチャ
- 管理者権限が不要な実行
BuildKit は moby プロジェクトが作成したビルドツールキットであり、Docker とは独立しているプロジェクトで開発されたものです。
Docker や Docker Compose で正式に利用できるようになりつつあるため、しばしば Docker の新しいビルド機能として紹介されています。
BuildKit の概要
BuildKit は buildkitd
デーモンと buildctl
クライアントにより構成されています。
buildkitd
はデフォルトで gRPC による通信を受け付けます。
BuildKit のビルドは LLB(Low-Level intermediate build format)※ と呼ばれる中間形式のバイナリを基にして行われます。
LLB は DAG (有向非巡回グラフ)構造を持つため、ビルドステップの依存関係グラフを定義するために使用されます。
※ LLB の名称は docker con 18 の発表資料に書かれた資料 の内容を採用し、moby project ブログ Introducing BuildKit にある内容は採用しませんでした
Frontend と呼ばれる BuildKit に含まれるコンポーネントが Dockerfile 等のハイレベルの言語で記述されたファイルを LLB へ変換します。
Frontend には gateway.v0
や dockerfile.v0
(開発中のみの存在) が存在し、dockerfile frontend を使うとホスト内のファイルが参照でき、gateway frontend を使うとネットワーク越しのファイルを参照することが出来ます。
ビルドした結果は次に示すとおり、いくつかの形式で出力でき、オプションによりレジストリへプッシュしたり、圧縮・展開などを行うことが出来ます。
- ファイル
- OCI tarball
- Docker tarball (
docker load
出来る形式) - containerd image store
ビルドキャッシュは出力先と形式をいくつか指定でき、出力先と形式の違いにより定義された次に示す exporter
が用意されています。
- inline exporter
- キャッシュをイメージへ埋め込み、レジストリへ共にプッシュする
- (キャッシュをインポートする際は
--import-cache type=registry,ref=...
を指定する必要があるため注意)
- registry exporter
- イメージとキャッシュを分離してプッシュする
- local exporter
- ローカルディレクトリへ出力する
Docker における BuildKit の導入
導入メリット
BuildKit を使って Docker コンテナイメージをビルドするメリットは具体的に次のとおりです。
- 複数ステージのビルドを並列実行できる
- 機密情報をビルド成果物(キャッシュを含む)に残さない仕組み(Dockerfile 内の
RUN --mount
コマンド)が使える(参考1, 参考2)- ローカルファイルをビルド時のみに使える
- リモートファイルをビルド時にSSH接続して取得できる
- ビルドキャッシュのインポート/エクスポートが出来る
- 分散ビルドが出来る (将来的に出来るようになること)
Docker バージョンに対する BuildKit 使用方法
Docker 18.06 にて BuildKit が試験的に導入され、Docker 18.09 以降で正式に導入されました。
このバージョンでは環境変数を設定して一時的に BuildKit を有効にする方法と、設定ファイルによりデフォルトで有効にする方法があります。
- 環境変数を設定する方法
-
DOCKER_BUILDKIT=1
を設定する - 例えば
DOCKER_BUILDKIT=1 docker build .
により一時的に BuildKit を使ってビルド出来ます
-
- 設定ファイルに設定する方法
-
/etc/docker/daemon.json
にfeatures.buildkit: true
を設定する - 例えば以下の値を設定して dockerd を再起動します
- ※
docker build --help
を実行して--secret
オプションが表示されれば BuildKit が使えるようになっていることが分かります
- ※
-
{
"features": {
"buildkit": true
}
}
また、Docker 19.03 ではさらに機能が強化された上で docker/buildx
プラグインとして実装されました。(Docker Buildx)
このバージョンでは試験機能 (experimental features) モードを有効にすることで docker buildx <COMMAND>
により BuildKit を使うことが出来ます。
例えば docker buildx build .
により BuildKit を使ったビルドを行うことが出来ます。
試験機能モードを有効にするためには config.json (デフォルトでは ~/.docker/config.json
に保存される) に "experimental": "enabled"
を指定します。
このとき、環境変数 DOCKER_BUILDKIT=1
を設定する必要はありません。また dockerd の再起動は不要です。
{
"experimental": "enabled"
}
$ docker -v
Docker version 19.03.6, build 369ce74a3c
$ docker buildx --help
Usage: docker buildx COMMAND
Build with BuildKit
Management Commands:
imagetools Commands to work on images in registry
Commands:
bake Build from a file
build Start a build
create Create a new builder instance
inspect Inspect current builder instance
ls List builder instances
rm Remove a builder instance
stop Stop builder instance
use Set the current builder instance
version Show buildx version information
Run 'docker buildx COMMAND --help' for more information on a command.
ビルドコマンド
buildx プラグインを使わない場合は通常の docker build
コマンドによりビルド出来ます。
$ DOCKER_BUILDKIT=1 docker build .
buildx プラグインを使う場合は docker buildx build
によりビルド出来ます。
$ docker buildx build .
ビルドキャッシュの利用
デフォルトでビルドキャッシュは BuildKit内部 に保存されます。
ここで、ビルドキャッシュが保存される場所は docker
ドライバを使う場合は dockerd が動作するホスト内であり、 docker-container
ドライバを使う場合は BuildKit が動作するコンテナ内です。
外部のビルドキャッシュに保存する場合は --cache-to
で指定します。(オプションは docker-container
ドライバの場合のみ指定可能)
外部のビルドキャッシュを使用する場合は --cache-from
オプションにより指定します。
Docker Hub にキャッシュを保存して効率よくビルドする方法
Docker Hub にキャッシュを保存して効率よくビルドする方法は次のとおりです。
$ DOCKERHUB_REPOSITORY=<YOUR_NAME>/<REPOSITORY_NAME>
$ docker buildx build \
--tag ${DOCKERHUB_REPOSITORY} \
--platform linux/amd64 \
--cache-from type=registry,ref=${DOCKERHUB_REPOSITORY} \
--cache-to type=inline \
--push \
.
Docker Hub のキャッシュを使ってビルドし、ローカルに保存する方法
Docker Hub のビルドキャッシュを使ってビルドし、ローカルに保存する方法は次のとおりです。
ビルドしたイメージにバージョン等のタグをつける場合に有効です。
$ DOCKERHUB_REPOSITORY=<YOUR_NAME>/<REPOSITORY_NAME>
$ docker buildx build \
--tag ${DOCKERHUB_REPOSITORY} \
--platform linux/amd64 \
--cache-from type=registry,ref=${DOCKERHUB_REPOSITORY} \
--load \
.
$ VERSION=<VERSION(ex. "0.3.0")>
$ docker image tag ${DOCKERHUB_REPOSITORY} ${DOCKERHUB_REPOSITORY}:${VERSION}
BuildKit のビルドキャッシュと Docker のビルドキャッシュは別の場所に保存されますが、docker history で同様に確認することが出来ます。
ビルドキャッシュの確認
docker history
コマンドにイメージ名を指定すると出力される結果の IMAGE
列を見るとイメージをビルドした時に作成されたキャッシュが確認できます。(docker image ls -a
コマンドによりキャッシュされたイメージが確認できます)
これらのキャッシュはイメージが削除されると破棄されます。
$ docker history 2cae1c7f3bb1
IMAGE CREATED CREATED BY SIZE COMMENT
2cae1c7f3bb1 30 seconds ago /bin/sh -c #(nop) CMD ["backup" "prune" "li… 0B
8a8ba5ba5ed8 30 seconds ago /bin/sh -c #(nop) ENTRYPOINT ["/opt/bin/ent… 0B
96a1b883328d 30 seconds ago /bin/sh -c #(nop) WORKDIR /opt/bin 0B
aa7a6ac5bc35 31 seconds ago /bin/sh -c #(nop) COPY dir:ebefed72581170cd5… 10.6kB
c14edf2fc444 31 seconds ago /bin/sh -c #(nop) ENV AWS_DEFAULT_REGION=ap… 0B
64972185d8d4 31 seconds ago |2 CLOUD_SDK_URL=https://dl.google.com/dl/cl… 309B
b77564bb5b09 34 seconds ago |2 CLOUD_SDK_URL=https://dl.google.com/dl/cl… 5.13MB
f9a28314aea4 41 seconds ago |2 CLOUD_SDK_URL=https://dl.google.com/dl/cl… 251MB
6cceb62a25f0 About a minute ago /bin/sh -c #(nop) ARG CLOUD_SDK_URL=https:/… 0B
dbda18957072 About a minute ago /bin/sh -c #(nop) ARG CLOUD_SDK_VERSION=281… 0B
76f445c07602 About a minute ago /bin/sh -c pip install awscli 68.6MB
fb8673a8e244 About a minute ago /bin/sh -c apk add --no-cache coreutils … 165MB
0fe716eb3f28 About a minute ago /bin/sh -c #(nop) LABEL maintainer=WESEEK <… 0B
6d1ef012b567 12 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 12 months ago /bin/sh -c #(nop) ADD file:aa17928040e31624c… 4.21MB
$ docker history 7f2334605161
IMAGE CREATED CREATED BY SIZE COMMENT
7f2334605161 40 seconds ago CMD ["backup" "prune" "list"] 0B buildkit.dockerfile.v0
<missing> 40 seconds ago ENTRYPOINT ["/opt/bin/entrypoint.sh"] 0B buildkit.dockerfile.v0
<missing> 40 seconds ago WORKDIR /opt/bin 0B buildkit.dockerfile.v0
<missing> 40 seconds ago COPY bin /opt/bin # buildkit 10.6kB buildkit.dockerfile.v0
<missing> 40 seconds ago ENV AWS_DEFAULT_REGION=ap-northeast-1 0B buildkit.dockerfile.v0
<missing> 40 seconds ago RUN |2 CLOUD_SDK_VERSION=281.0.0 CLOUD_SDK_U… 309B buildkit.dockerfile.v0
<missing> 42 seconds ago RUN |2 CLOUD_SDK_VERSION=281.0.0 CLOUD_SDK_U… 5.13MB buildkit.dockerfile.v0
<missing> 47 seconds ago RUN |2 CLOUD_SDK_VERSION=281.0.0 CLOUD_SDK_U… 251MB buildkit.dockerfile.v0
<missing> 57 seconds ago ARG CLOUD_SDK_URL=https://dl.google.com/dl/c… 0B buildkit.dockerfile.v0
<missing> 57 seconds ago ARG CLOUD_SDK_VERSION=281.0.0 0B buildkit.dockerfile.v0
<missing> 57 seconds ago RUN /bin/sh -c pip install awscli # buildkit 68.6MB buildkit.dockerfile.v0
<missing> About a minute ago RUN /bin/sh -c apk add --no-cache coreut… 165MB buildkit.dockerfile.v0
<missing> About a minute ago LABEL maintainer=WESEEK <info@weseek.co.jp> 0B buildkit.dockerfile.v0
<missing> 12 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 12 months ago /bin/sh -c #(nop) ADD file:aa17928040e31624c… 4.21MB
ビルドキャッシュの削除
docker
ドライバを使う場合は docker builder prune
コマンドにより削除できます。
$ docker builder prune
WARNING! This will remove all dangling build cache. Are you sure you want to continue? [y/N] y
Deleted build cache objects:
lvle82pkre0lf8kf0mfk7oql6
e28zwrr5mc29ypc8rx9kiexre
kyjw2zvkv5nnm0r58b3jdrxsq
Total reclaimed space: 142B
docker-container
ドライバを使う場合は BuildKit コンテナのシェルにて buildctl prune
コマンドを実行することで削除できます。(docker builder prune
で消せればよいと思うのですが消えません...)
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fec0ae39de39 moby/buildkit:buildx-stable-1 "buildkitd" 2 days ago Up 2 days buildx_buildkit_elated_dewdney0
$
$ docker exec buildx_buildkit_elated_dewdney0 buildctl prune
ID RECLAIMABLE SIZE LAST ACCESSED
j9uce3ql87wwe5s4mjghj2kct* true 8.22kB
yvmcrshb44p02r2wggdpv7z5f true 8.48kB
cwnkq42al3idkfeas0g0miv3z* true 4.21kB
yr2wyc3rw11ehbp3yox7ffgx4* true 4.10kB
sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10 true 8.77MB
Total: 8.79MB
非 BuildKit におけるキャッシュイン・キャッシュアウトの仕組み
docker build
では同一 ID のイメージがある場合にキャッシュ機能が有効になります。
そのため、マルチステージビルドでキャッシュを使う場合は各ステージのイメージを push する必要があります。
docker build の --cache-from
オプションを使ってキャッシュ機能を有効にする場合は次のようになります。
$ DOCKERHUB_REPOSITORY=<YOUR_NAME>/<REPOSITORY_NAME>
$ docker pull ${DOCKERHUB_REPOSITORY}
$ docker build \
--tag ${DOCKERHUB_REPOSITORY} \
--cache-from ${DOCKERHUB_REPOSITORY} \
.
$ DOCKERHUB_REPOSITORY=<YOUR_NAME>/<REPOSITORY_NAME>
$ docker pull ${DOCKERHUB_REPOSITORY}
$ docker pull ${DOCKERHUB_REPOSITORY}:stage1
: 各ステージのビルド結果を pull する
$ docker build \
--tag ${DOCKERHUB_REPOSITORY} \
--cache-from ${DOCKERHUB_REPOSITORY} \
--cache-from ${DOCKERHUB_REPOSITORY}:stage1 \
: 各ステージのビルド結果を指定する
.
Dockerfile はステージの数によらず上から下に 1 ステップずつビルドされます。
これは docker build
コマンドの実行結果の Step X/Y
を見ることで確認できます。
キャッシュは各ステップごとに作成されます。
また、キャッシュには ID が採番され、ID が同じであればキャッシュが利用されます。
ID が異なればキャッシュは使われず、同一ステージにおける以降の全てのステップではキャッシュが利用されません。
ID は docker build
コマンドの実行結果内の ---> dd025cdfe837
等と表示されます。
もしキャッシュが利用されたときは ---> Using cache
と表示されます。
キャッシュ ID はステップのコマンド内容によって次のように作成され、ID が変わる条件は次のとおりです。
ID が変わるとキャッシュアウトされます。
- FROM コマンドではベースとなるイメージの内容がキャッシュ ID に反映される
- つまり、イメージが変わるとキャッシュ ID が変わる
- ADD, COPY コマンドでは COPY する元のファイル内容がキャッシュ ID に反映される
- つまり、ファイル内容が変わるとキャッシュ ID が変わる
- LABEL, ENV, ARG コマンドでは設定する値がキャッシュ ID に反映される
- つまり、設定する値が変わるとキャッシュ ID が変わる
- RUN, CMD, ENTRYPOINT, EXPOSE, USER コマンドではコマンド内容がキャッシュ ID に反映される
- つまり、実行するコマンドが変わるとキャッシュ ID が変わる
- そのため、コマンドを実行する度に結果が変わる(冪等ではない)場合はキャッシュされることを注意する必要があります
※ もし COPY . .
を使う場合 Dockerfile が含まれないように .dockerignore を用意することをお勧めします。Dockerfile が COPY 元に含まれるとファイル変更によりキャッシュアウトするためです。
※ キャッシュを pull すると Docker Hub の pull 数が 1 より大きい値になっているようです。ただ、キャッシュが空の時に pull しても数が不定であり、仕組みがよく分かりません。('2020/03/15現在)
BuildKit におけるキャッシュイン・キャッシュアウトの仕組み
docker buildx build
ではイメージを pull しなくても --cache-from
を指定することでキャッシュが利用できます。
また、マルチステージビルドの場合も結果のイメージに各ステージのキャッシュを埋め込むことが出来ます。
- CMD, ENTRYPOINT, EXPOSE, ENV, LABEL, USER が変わってもキャッシュに影響しない
- ARG は値が変わっただけではキャッシュに影響しない
- 但し、ARG を利用したコマンドの内容が変わることで、それらのコマンドの条件に応じてキャッシュが利用されるかどうかが分かれます
これは docker buildx build
コマンド実行結果の [builder]
や [stage-X X/Y]
に上記コマンドが表示されないことから確認できます。
ビルダインスタンスを操作する (docker/buildx
プラグインで利用可能)
ビルダインスタンスとは独立したビルド環境のことです。
docker/buildx
プラグインではビルダインスタンスを操作することができます。
ビルダインスタンスを確認する
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
default * docker
default default running linux/amd64, linux/386
ビルダインスタンスを作成する
利用中の docker 設定がdefault の値として適用されます。
default は docker
ドライバが使われますが、次のようにいくつか機能が制限されています。
-
--output
オプションで使用できるのは local, tarball exporter, image exporter のみである -
--cache-from
オプションで使用できるのは registry type のみである -
--cache-to
オプションで使用できるのは inline type のみである
docker-container
ドライバを使うにはビルダインスタンスを作成する必要があります。
$ docker buildx create --name docker-container --driver docker-container --use
docker-container
# ビルダインスタンスが作成され、使用するように設定されていることを確認する
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
docker-container * docker-container
docker-container0 unix:///var/run/docker.sock inactive
default docker
default default running linux/amd64, linux/386
# docker-container が起動していないことを確認する (docker-container ビルダインスタンスの STATUS が inactive である)
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 一度ビルドを行ってみる
$ docker buildx build .
: <snip>
# すると、コンテナが起動して STATUS が running になる
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3622af1006df moby/buildkit:buildx-stable-1 "buildkitd" About a minute ago Up About a minute buildx_buildkit_docker-container0
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
docker-container * docker-container
docker-container0 unix:///var/run/docker.sock running linux/amd64, linux/386
default docker
default default running linux/amd64, linux/386
# ちなみに、コンテナが停止した状態だと STATUS は stopped になる
$ docker stop buildx_buildkit_docker-container0
buildx_buildkit_docker-container0
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
docker-container * docker-container
docker-container0 unix:///var/run/docker.sock stopped
default docker
default default running linux/amd64, linux/386
Docker Compose における BuildKit の利用
BuildKit による Docker コンテナイメージビルドのメリットは具体的に次のとおりです。
- ステージビルドを並列実行できる
- 機密情報をビルド成果物(キャッシュを含む)に残さない仕組み(Dockerfile 内の
RUN --mount
コマンド)が使える(参考)- ローカルファイルをビルド時のみに使える
- リモートファイルをビルド時にSSH接続して取得できる
Docker Compose 1.25.0 にて docker-compose build 時に BuildKit が使用できる方法が追加されました。
Add BuildKit support, use DOCKER_BUILDKIT=1 and COMPOSE_DOCKER_CLI_BUILD=1
[出典] https://github.com/docker/compose/releases/tag/1.25.0
環境変数を設定することで Docker Compose において BuildKit を使ってビルドすることができます。
ただし、Docker が BuildKit を使えるように設定されていることが前提です。
また、一方で docker buildx bake
コマンドにより docker-compose.yml を読み取ってビルドすることが出来ます。
- 環境変数を設定する方法
-
COMPOSE_DOCKER_CLI_BUILD=1
を設定する - 例えば
COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build
により BuildKit を使ってビルド出来ます
-
-
docker/buildx
プラグインを使う方法- buildx bake の対象ファイルとして docker-compose.yml を指定して実行します
- 例えば
docker buildx bake -f docker-compose.yml
を実行すると BuildKit を使ってビルド出来ます
もともと Docker Compose は docker/cli
は使わず、docker-py を使って dockerd の API にアクセスしますが、環境変数を使って BuildKit を使う仕組みは docker/cli
を使います。
これは、執筆当時の '20/03/09 現在では docker-py は BuildKit に対応していなかった(参考) ため、docker-compose にて BuildKit を使うために docker/cli
を使用する方法が実装されたためです。(参考1, 参考2, 参考3)
# docker-compose が docker-py を利用していることの確認
$ docker-compose version
docker-compose version 1.25.4, build 8d51620a
docker-py version: 4.1.0
CPython version: 3.7.5
OpenSSL version: OpenSSL 1.1.0l 10 Sep 2019
Docker Compose における BuildKit ビルド時に外部キャッシュを利用する
- 環境変数
COMPOSE_DOCKER_CLI_BUILD=1
を使う場合- docker-compose.yml 内の
services.<service名>.build.cache_from
に外部キャッシュを指定します - 例えば以下の docker-compose.yml を記述して
docker-compose build
を実行するとキャッシュが有効になります - なお、
docker-compose up --build
でもキャッシュを利用してビルドすることが出来ます
- docker-compose.yml 内の
-
docker/buildx
プラグインを使う方法- 事前に外部キャッシュを使ってベースイメージをビルドした後に docker-compose.yml 内のサービスをビルドします
- 例えば以下の docker-compose.yml の場合、
docker buildx build --cache-from
を実行してベースイメージをビルドした後にdocker buildx bake -f docker-compose.yml
を実行します
環境変数を使う場合、docker-compose build
を実行した際に、次の warning メッセージが表示されれば BuildKit によるビルドが有効になっています。
WARNING: Native build is an experimental feature and could change at any time
# cache_from は v3.2 以降で利用可能です (https://docs.docker.com/compose/compose-file/)
version: '3.2'
services:
app:
build:
context: .
cache_from:
# イメージは適当です。後続で紹介する Dockerfile を使ってビルドした結果を指定しましょう
- ryu310/github_action_sandbox
FROM alpine:3.11.3
LABEL maintainer="Ryu Sato"
COPY scripts/entrypoint.sh /root
CMD ["/root/entrypoint.sh"]
version: '3'
services:
app:
build:
context: .
FROM alpine:3.11.3
LABEL maintainer="Ryu Sato"
COPY scripts/entrypoint.sh /root
CMD ["/root/entrypoint.sh"]
$ DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build
WARNING: Native build is an experimental feature and could change at any time
: <snip>
=> CACHED [2/2] COPY scripts/entrypoint.sh /root
: <snip>
Successfully built d1acbb2a8544a28dd4a4d300a6caab3f0c9aabe030a96e6621cbafd9114f1f0a
$ docker buildx build --cache-from type=registry,ref=ryu310/github_action_sandbox .
: <snip>
=> CACHED [2/2] COPY scripts/entrypoint.sh /root
$ docker buildx bake -f docker-compose.yml
: <snip>
=> CACHED [2/2] COPY scripts/entrypoint.sh /root