Docker BuildKitを使う

はじめに

Dockerの 18.09 以降にはBuildKitという機能が存在しますが、今までなんとなく使ってただけなのでちゃんとまとめてある程度理解しておきたいなと

Docker BuildKitとは

そもそもBuildKitそのものはDocker Engineとは別でMoby Projectで開発されていたものです。 キャッシュや処理の並列化、アーキテクチャの見直しによってソースコードをビルドアーティファクトへ変換する処理をより効率よく行なうことができるようになったり、Dockerfileの拡張文法を提供していたりします。Dockerの 18.09 以降はDocker Engineに拡張として取り込まれ、特に特別なインストールなどはなしに利用することができます。

何ができるん?

つまるところ、BuidKitはどういうことを可能にしてくれるのかということを以下にまとめます。

  • ビルドの高速化される
  • ビルドの標準出力に各ステップの実行時間が出力される
  • Dockerfileの拡張文法が利用可能になる(後述)

使ってみる

環境

実行環境は以下の通り

$ docker version
Client: Docker Engine - Community
 Version:           19.03.12
 API version:       1.40
 Go version:        go1.13.10
 Git commit:        48a66213fe
 Built:             Mon Jun 22 15:45:36 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.12
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.10
  Git commit:       48a66213fe
  Built:            Mon Jun 22 15:44:07 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal

$ uname -srvmpio
Linux 5.4.0-53-generic #59-Ubuntu SMP Wed Oct 21 09:38:44 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
 

Dockerfileを用意しておく

Docker BuildKitの機能を試すためにいかのDockerfileをベースにします。
特に明示してなければ以下のDcokerfileを利用してビルドを行っています。

FROM centos:7 
RUN yum install -y java 

BuildKit有効化

環境変数での有効化

BuildKitは環境変数DOCKER_BUILDKIT=1と設定しておくと有効化されます。例えば単発のビルド時に有効化したい場合は以下のようにします。

$DOCKER_BUILDKIT=1 docker build .

デフォルトでの有効化

デフォルトで有効化するためには/etc/docker/daemon.jsonfeatureの項目をtrueに設定します。

{ "features": { "buildkit": true } }

今回はデフォルトとの対比を行いたいのでこちらは使わず環境変数の方で指定して、BuildKitを使いたいと思います。

出力結果のが変わる

まずは普通にdocker buildした場合は以下のような出力になります。

$ docker build -t java-centos .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM centos:7
 ---> 7e6257c9f8d8
Step 2/2 : RUN yum install -y java
 ---> Using cache
 ---> 68dff4f570e9
Successfully built 68dff4f570e9
Successfully tagged java-centos:latest

次にBuildKitを有効化してDockerBuildを実行します。

$ DOCKER_BUILDKIT=1 docker build -t java-centos .                                                                                                      
[+] Building 0.0s (6/6) FINISHED                                                                                                                                                                           
 => [internal] load .dockerignore                                                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                                                       0.0s
 => [internal] load build definition from Dockerfile                                                                                                                                                  0.0s
 => => transferring dockerfile: 37B                                                                                                                                                                   0.0s
 => [internal] load metadata for docker.io/library/centos:7                                                                                                                                           0.0s
 => [1/2] FROM docker.io/library/centos:7                                                                                                                                                             0.0s
 => CACHED [2/2] RUN yum install -y java                                                                                                                                                              0.0s
 => exporting to image                                                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                                                               0.0s
 => => writing image sha256:fc5361ee029af85ff2d83403afe003214d2dfcb539216b3fcfa1349852ed614a                                                                                                          0.0s
 => => naming to docker.io/library/java-centos   

出力する項目がすこし変わっているのと、各工程にどのくらいの時間がかかったのかの計測結果が出力されるようになります。
また、--progress=plainを付け加えることで出力形式をプレーン?なものに変えることもできるみたいです。

$ DOCKER_BUILDKIT=1 docker build  --progress=plain  -t java-centos .
#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 37B done
#2 DONE 0.0s

#1 [internal] load .dockerignore
#1 transferring context: 2B done
#1 DONE 0.0s

#3 [internal] load metadata for docker.io/library/centos:7
#3 DONE 0.0s

#4 [1/2] FROM docker.io/library/centos:7
#4 DONE 0.0s

#5 [2/2] RUN yum install -y java
#5 CACHED

#6 exporting to image
#6 exporting layers done
#6 writing image sha256:fc5361ee029af85ff2d83403afe003214d2dfcb539216b3fcfa1349852ed614a done
#6 naming to docker.io/library/java-centos done
#6 DONE 0.0s

(いまのところこいつの利点があんまりわかってないが、余計な=>がなくなってるだけのかな...?)

BuildKitのフロンドエンドの置き換え

ここで言うフロントエンドとはBuildKitの中で動くビルドディフィニションをLLB(BuildKitが生成する中間バイナリフォーマット)に変換するコンポーネントのことを指します。Docker目線でいえばDockerfileをバイナリに変換するためのコンポーネントのことを指します。
Dcokerfileの先頭行に以下のような形式のコメントを追加することでこのフロントエンドを置き換えることができます。

# syntax = <frontend image>

現状Dockerfileのフロントエンドのイメージは以下の2つが用意されているようです。

後述しますが、フロントエンドを置き換えることによってDockerfile内でエクスペリメンタルな文法を利用することが可能となります。

フロントエンドを置き換えてエクスペリメンタルの機能を利用する(実験機能)

BuildKitを使ったDockerfileのビルドにおいて別フロントエンドを使うことで拡張の機能を利用することができます。

フロントエンドの置き換え

フロントエンドの置き換えに関しては前述していますが、今回使いたい機能を利用するために、Dockerfileの先頭にコメントを追加します。

# syntax=docker/dockerfile:experimental

FROM centos:7 
RUN yum install -y java 

これで拡張文法とその機能を利用することが可能です。

拡張文法

置き換えたフロントエンドで便利そうな機能をいくつかまとめます。

RUN --mount=type=bind

--mount=type=bindはビルドコンテクスト(Dockerfileが置かれているワーキングディレクトリ)、もしくは、ビルドされるイメージコンテナのディレクトリをバインドすることができます。バインドされるディレクトリはデフォルトはリードオンリーです。

下記のような項目を,でつないで利用することができます。

項目 説明
target(必須) Dockerコンテナ内のマウント先のパス
from ビルドストレージ、もしくはソースのルート。デフォルトはビルドのコンテクストにバインドされます
source fromの中のソースパス、デフォルトのソースパスはfromのルートディレクト
rw,readwrite マウントされたディレクトリに書き込むことを許可します。ただし、書き込んだデータは後に削除されます

例えば下記のDockerfileではビルドコンテキストにあるDockerfileの内容をtext.txtに書き写しています。

# syntax=docker/dockerfile:experimental

FROM busybox

RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt

下記のように実行するとビルドコンテキストがDokcerコンテナ内の/tmpバインディングされ、Dockerfileの内容がtext.txtに書き移すことができます。

$ DOCKER_BUILDKIT=1 docker build   -t busybox-tmp .
[+] Building 2.1s (9/9) FINISHED                                                                                                                                                                           
 => [internal] load build definition from Dockerfile                                                                                                                                                  0.0s
 => => transferring dockerfile: 37B                                                                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                                                       0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                                                                                                 1.8s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:de85b2f3a3e8a2f7fe48e8e84a65f6fdd5cd5183afa6412fff9caa6871649c44                                                            0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                                                                                                                                     0.0s
 => [internal] load build context                                                                                                                                                                     0.0s
 => => transferring context: 31B                                                                                                                                                                      0.0s
 => [1/2] FROM docker.io/library/busybox                                                                                                                                                              0.0s
 => CACHED [2/2] RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt                                                                                                                       0.0s
 => exporting to image                                                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                                                               0.0s
 => => writing image sha256:d75bb6670538d7af739b6f201d170a5fa38128e7cbbcf56ef9448682695d0b36                                                                                                          0.0s
 => => naming to docker.io/library/busybox-tmp

$ docker run busybox-tmp cat text.txt
# syntax=docker/dockerfile:experimental

FROM busybox

RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt

この機能のユースケースとしてはDockerコンテナ内に必要は無いけど実行はしたいスクリプトがあるディレクトリをバインドしたりすることが考えられます。
ちなみにですが、targetにコンテナ内に元来存在しないディレクトリを指定すると勝手にディレクトリを作成しそこに対してビルドコンテキスト(や指定されたビルドストレージ、もしくはイメージのディレクトリ)をバインドしてくれます。

RUN --mount=type=cache

コンパイラーやパッケージマネージャーのキャッシュディレクトリをバインドすることができます。

下記のような項目を,でつないで利用することができます。

項目 説明
id キャッシュを識別するためのID
target(必須) Dockerコンテナ内のマウント先のパス
ro,readonly セットされていればキャッシュがリードオンリーになります
from キャッシュマウントを行なうビルドストレージ。デフォルトは空のディレクト
source マウントを行なうfromの中のサブパス。デフォルトはfromのルート
mode 新しいキャッシュディレクトリのファイルモード、デフォルトは0755
uid 新しいキャッシュディレクトリのUserID。デフォルトは0
gid 新しいキャッシュディレクトリのグループID。デフォルトは0

例えば下記のDockerfileはmavenのm2ディレクトリをキャッシュして、2回目以降は.m2のキャッシュが/root/.m2にマウントされるようになります。

# syntax = docker/dockerfile:1.1-experimental
FROM maven:3.6.2-jdk-8-slim AS build-env
ADD . /javasoerceroot
WORKDIR /javasorceroot

RUN --mount=type=cache,target=/root/.m2 mvn clean package

aptパッケージをキャッシュする場合以下のようにします。

# syntax = docker/dockerfile:experimental
FROM ubuntu

RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
  apt update && apt-get --no-install-recommends install -y gcc

RUN --mount=type=tmpfs

tmpfsをコンテナへバインドすることができます。

下記のような項目を,でつないで利用することができます。

項目 説明
target(必須) Dockerコンテナ内のマウント先のパス

RUN --mount=type=secret

秘密鍵のファイルようなクレデンシャルファイルに対して、をイメージの中に残すこと無くバインドすることができます。

項目 説明
id シークレットのID。ターゲットパスのデフォルトベース名として利用されます。
target Dockerコンテナ内のマウント先のパス。デフォルトでは/run/secrets/ + idとなります。
required trueでセットすると、シークレットが有効で無かった場合に命令がエラーとして取り扱われるようになります。デフォルトはfalseです
mode シークレットのファイルモードデフォルトは0400
uid シークレットのユーザID。デフォルトは0
gid シークレットのグループID。デフォルトは0

例えばAWSのS3へアクセスしたいようなユースケースでは以下のようにします。

# syntax = docker/dockerfile:experimental
FROM python:3
RUN pip install awscli
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials aws s3 cp s3://... ...

docker build時に--secretフラグを用いてバインドするクレデンシャルなファイルを渡します。

$ docker build  --secret id=aws,src=$HOME/.aws/credentials .

その他機能はこちらをご覧ください。

参考資料