Docker「RUN --mount=type=cache,target」を試してみた話


概要

docker buildのキャッシュを試してみた際の記事
記事内では「RUN --mount=type=secret」や「RUN --mount=type=ssh」については取り上げていません

検証環境

docker for macを使用しています。

$ uname -a
Darwin mbp01 19.0.0 Darwin Kernel Version 19.0.0: Thu Oct 17 16:17:15 PDT 2019; root:xnu-6153.41.3~29/RELEASE_X86_64 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           19.03.4
 API version:       1.40
 Go version:        go1.12.10
 Git commit:        9013bf5
 Built:             Thu Oct 17 23:44:48 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.4
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.10
  Git commit:       9013bf5
  Built:            Thu Oct 17 23:50:38 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.10
  GitCommit:        b34a5c8af56e510852c35414db4c1f4fa6172339
 runc:
  Version:          1.0.0-rc8+dev
  GitCommit:        3e425f80a8c931f88e6d94a8c831b9d5aa481657
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

検証

(パッケージマネージャのキャッシュを効かせる)

例えばこんなDockerfileがあるとします。
(dnfを使って適当にパッケージをインストールするだけ)

FROM centos:8
RUN echo "test1"
RUN dnf install -y \
    gcc gcr lvm2 clang

これをbuildkitを有効にした状態でビルドしてみます
すると手元の環境で約50秒程度でビルドが完了しました。

docker image build -t test01 .
[+] Building 49.4s (7/7) FINISHED
 => [internal] load .dockerignore                                                                                                 0.0s
 => => transferring context: 2B                                                                                                   0.0s
 => [internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 114B                                                                                              0.0s
 => [internal] load metadata for docker.io/library/centos:8                                                                       2.1s
 => [1/3] FROM docker.io/library/centos:8@sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9                 0.0s
 => CACHED [2/3] RUN echo "test1"                                                                                                 0.0s
 => [3/3] RUN dnf install -y  gcc gcr lvm2 clang                                                                                 44.1s
 => exporting to image                                                                                                            3.2s
 => => exporting layers                                                                                                           3.2s
 => => writing image sha256:d122ae7f88c40310b925ca575df754f3732a18fb26a488ba11c8de7bf174d1e4                                      0.0s
 => => naming to docker.io/library/test01                                                                                         0.0s
>>> elapsed time 50s

その後に下記のようにaptの前に新たにレイヤーが作られるようにdockerfileを修正します。

FROM centos:8
RUN echo "test2" # 修正
RUN dnf install -y \
    gcc gcr lvm2 clang

そして同じコマンドでビルドすると同じくらいの時間がかかりました。
(イメージレイヤについては下記をご参照ください)

知らないと損する Docker イメージのレイヤ構造とは

$ docker image build -t test02 .
[+] Building 52.6s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 114B                                                                                              0.0s
 => [internal] load .dockerignore                                                                                                 0.0s
 => => transferring context: 2B                                                                                                   0.0s
 => [internal] load metadata for docker.io/library/centos:8                                                                       3.0s
 => CACHED [1/3] FROM docker.io/library/centos:8@sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9          0.0s
 => [2/3] RUN echo "test2"                                                                                                        0.7s
 => [3/3] RUN dnf install -y  gcc gcr lvm2 clang                                                                                 45.7s
 => exporting to image                                                                                                            3.0s
 => => exporting layers                                                                                                           3.0s
 => => writing image sha256:85b29e834097a17f7311c775bed1b1a0604ddacc6af81a19267edff0ab4ae7cf                                      0.0s
 => => naming to docker.io/library/test02                                                                                         0.0s
>>> elapsed time 52s

一番時間がかかるのはおわかりの通りgccのインストールです。
gccのインストールにはたくさんの依存パッケージが存在するのでとても時間がかかります。

dnf(yum)はとても賢いです。いい感じに独自のキャッシュを持っていてそこを有効活用することで高速にビルドが可能です。
そんな時に役立つのが「--mount=type=cache,target」です。

オプション

Option Description
id ユニークなIDを指定
target (必須) マウントするパスを指定
ro,readonly 読み取り専用オプション
sharing 共有する場合の方式(詳細は下記ページをご参照ください🙇‍♂️)
from キャッシュのベースとして使用するディレクトリ

これを使うことでこれまでDockerfile内の2行目で行っていたechoのキャッシュを破棄しつつ、
パッケージマネージャのキャッシュを活かしたビルドが可能になります。

Dockerfile例です。
まだ非標準命令のため1行目に「# syntax = docker/dockerfile:experimental」という記述が必要とのことです。
「--mount=type=cache,target」のターゲットにキャッシュしたいディレクトリ(aptなどの場合は適宜読み替えてください)

# syntax = docker/dockerfile:experimental
FROM centos:8
RUN echo "test3"
RUN \
  --mount=type=cache,target=/var/cache/dnf \
  --mount=type=cache,target=/var/lib/dnf \
  dnf install -y \
    gcc gcr lvm2 clang

1回目のビルド

1回目はキャッシュの構築です。上記と同じく50秒程度かかることがわかりました。

$ docker image build -t test03 .
[+] Building 52.3s (9/9) FINISHED
 => [internal] load .dockerignore                                                                                                 0.0s
 => => transferring context: 2B                                                                                                   0.0s
 => [internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 250B                                                                                              0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                             2.1s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:888f21826273409b5ef5ff9ceb90c64a8f8ec7760da30d1ffbe6c3  0.0s
 => [internal] load metadata for docker.io/library/centos:8                                                                       0.9s
 => CACHED [stage-0 1/3] FROM docker.io/library/centos:8@sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9  0.0s
 => [stage-0 2/3] RUN echo "test3"                                                                                                0.7s
 => [stage-0 3/3] RUN   --mount=type=cache,target=/var/cache/dnf   --mount=type=cache,target=/var/lib/dnf   dnf install -y  gcc  44.8s
 => exporting to image                                                                                                            3.1s
 => => exporting layers                                                                                                           3.1s
 => => writing image sha256:a4f1634b51036b0ae3107ac44884e980701a698c44315786c9eb42450f65ce51                                      0.0s
 => => naming to docker.io/library/test03

2回目のビルド

キャッシュを使わない検証同様にechoの部分を書き換えます。

# syntax = docker/dockerfile:experimental
FROM centos:8
RUN echo "test4" #修正
RUN \
  --mount=type=cache,target=/var/cache/dnf \
  --mount=type=cache,target=/var/lib/dnf \
  dnf install -y \
    gcc gcr lvm2 clang

上記を使用してビルド。
約20秒程度の短縮という結果となりました。

docker image build -t test04 .
[+] Building 35.7s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                                                              0.0s
 => => transferring dockerfile: 250B                                                                                              0.0s
 => [internal] load .dockerignore                                                                                                 0.0s
 => => transferring context: 2B                                                                                                   0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                             0.9s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:888f21826273409b5ef5ff9ceb90c64a8f8ec7760da30d1ffbe6c3  0.0s
 => [internal] load metadata for docker.io/library/centos:8                                                                       0.9s
 => CACHED [stage-0 1/3] FROM docker.io/library/centos:8@sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9  0.0s
 => [stage-0 2/3] RUN echo "test4"                                                                                                0.7s
 => [stage-0 3/3] RUN   --mount=type=cache,target=/var/cache/dnf   --mount=type=cache,target=/var/lib/dnf   dnf install -y  gcc  29.5s
 => exporting to image                                                                                                            2.9s
 => => exporting layers                                                                                                           2.9s
 => => writing image sha256:602d0e7bcc5b5701276bc0d25c853557d45b6925cb049cd4848b739647992ba0                                      0.0s
 => => naming to docker.io/library/test04                                                                                         0.0s
>>> elapsed time 35s

注目すべきはは「[stage-0 3/3] RUN --mount=type=cache,target=/var/cache/dnf」の部分です。
1回目は44秒程度かかったのに対して30秒程度で完了したことがわかります。

まとめ

今回はdnfでやってみましたが、pipとかパッケージ管理をテキストでやるようなパッケージマネージャなどでも有効かなと思いました。
(requirements.txtを頻繁に書き換える場合もある程度はキャッシュが効く?)
パッケージのアップデートやらキャッシュが効く部分(マルチステージビルドの記事が結構多い)での改善は楽しいです。
その辺はこれから試しながら遊んでいきたいと思います。

参考サイト