見出し画像

Dockerfileを改善して、ビルド時間の短縮をためしてみた

はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

マルチステージビルドで Docker イメージのサイズを削減してみたでは、マルチステージビルドで Docker イメージのサイズの削減をやってみた。
今回はその中で次回の記事で取り上げると書いていた「レイヤー・キャッシュ」について、実際にどのように Dockerfile を記述すると、レイヤーキャッシュを活用してビルド時間を短くできるか?を見ていきたいと思う。

Docker のレイヤー

Docker のレイヤーとは、以下の画像のように Dockerfile の COPY や RUN などの各コマンドで作成される層の事。

詳細はBest practices for writing Dockerfilesに書かれている。

A Docker image consists of read-only layers each of which represents a Dockerfile instruction. The layers are stacked and each one is a delta of the changes from the previous layer.(Docker イメージは読み取り専用のレイヤーで構成されており、それぞれが Dockerfile の命令を表しています。レイヤーは積み重ねられ、各レイヤーは前のレイヤーからの変更の差分となります。)

このレイヤー、再利用可能であればそれを再利用するという仕組みになっており、レイヤーを意識する事で効率のいい Dockerfile を書く事ができる。

以下ではどのようにレイヤーが再利用されるか?(キャッシュされるか?)について確認したいと思う。

Docker のレイヤーの再利用(レイヤーキャッシュ)の動きを確認してみる

今回は、レイヤーとそのキャッシュについて、以下の 2 つ場面でその動きを見ていく。

①Docker イメージのビルド時
②Docker イメージを pull する際

①Docker イメージのビルド時

まず、Docker イメージをビルドする時の動きを確認したいと思う。

以下のような Dockerfile があった時、初めてイメージをビルドする場合には、約 3.7 秒ほどかかった。

FROM node:16.19.0-alpine3.16

RUN mkdir /app
WORKDIR /app

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh
RUN echo "Hello World!"

ENTRYPOINT ["./entrypoint.sh"]
study@localhost:~/workspace/docker-kubernetes (layer-cache)
$ time docker build -t test:v1 .
Sending build context to Docker daemon  66.56kB
Step 1/7 : FROM node:16.19.0-alpine3.16
 ---> cdb7a20a9b54
Step 2/7 : RUN mkdir /app
 ---> Running in d5d1d0034e01
Removing intermediate container d5d1d0034e01
 ---> 3e21e949eb2d
Step 3/7 : WORKDIR /app
 ---> Running in 5120f42ce1e6
Removing intermediate container 5120f42ce1e6
 ---> 4ac4194a3e8d
Step 4/7 : COPY entrypoint.sh entrypoint.sh
 ---> 9dc7fedd1896
Step 5/7 : RUN chmod +x entrypoint.sh
 ---> Running in 6a2eed9a91e2
Removing intermediate container 6a2eed9a91e2
 ---> f84cba5af587
Step 6/7 : RUN echo "Hello World!"
 ---> Running in d6b2a6a0889c
Hello World!
Removing intermediate container d6b2a6a0889c
 ---> c31ffc29b1f2
Step 7/7 : ENTRYPOINT ["./entrypoint.sh"]
 ---> Running in bc7be8a06ee5
Removing intermediate container bc7be8a06ee5
 ---> c8bb19ec56c6
Successfully built c8bb19ec56c6
Successfully tagged test:v1

real    0m3.739s
user    0m0.041s
sys     0m0.083s

再度、同じ Dockerfile でイメージをビルドすると今度は約 0.2 秒しかかからなかった。
どうしてこれだけの違いが出るかだが、これは Docker イメージをビルドする際にレイヤー構造が作られ、そのレイヤーごとにビルドキャッシュが残っており、2 回目のビルド時にはそのキャッシュを利用しているため。
以下のログの中に"Using cache"というのがあるが、それがキャッシュが使われている部分になる。

study@localhost:~/workspace/docker-kubernetes (layer-cache)
$ time docker build -t test:v1 .
Sending build context to Docker daemon  66.56kB
Step 1/7 : FROM node:16.19.0-alpine3.16
 ---> cdb7a20a9b54
Step 2/7 : RUN mkdir /app
 ---> Using cache
 ---> 3e21e949eb2d
Step 3/7 : WORKDIR /app
 ---> Using cache
 ---> 4ac4194a3e8d
Step 4/7 : COPY entrypoint.sh entrypoint.sh
 ---> Using cache
 ---> 9dc7fedd1896
Step 5/7 : RUN chmod +x entrypoint.sh
 ---> Using cache
 ---> f84cba5af587
Step 6/7 : RUN echo "Hello World!"
 ---> Using cache
 ---> c31ffc29b1f2
Step 7/7 : ENTRYPOINT ["./entrypoint.sh"]
 ---> Using cache
 ---> c8bb19ec56c6
Successfully built c8bb19ec56c6
Successfully tagged test:v1

real    0m0.260s
user    0m0.041s
sys     0m0.066s

このレイヤーが作られてキャッシュが残るという動きについてはレイヤーごとになる。
例えば以下のように Dockerfile に RUN コマンドを追加して、レイヤーが 1 つ追加されるようにした場合でも、変更されていない部分のレイヤーは"Using cache"になっている事が確認できると思う。
そして新しく追加されたレイヤーの部分以降のレイヤーだけ再構築されている事も確認できる。
そのため、ビルド時間としては約 1.6 秒と、キャッシュない場合の一番最初のビルドよりも短い時間で済んでいる。

FROM node:16.19.0-alpine3.16

RUN mkdir /app
WORKDIR /app

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh
RUN echo "Hello World!"
RUN echo "Add echo" # ←追加

ENTRYPOINT ["./entrypoint.sh"]
study@localhost:~/workspace/docker-kubernetes (layer-cache)
$ time docker build -t test:v1 .
Sending build context to Docker daemon  66.56kB
Step 1/8 : FROM node:16.19.0-alpine3.16
 ---> cdb7a20a9b54
Step 2/8 : RUN mkdir /app
 ---> Using cache
 ---> 3e21e949eb2d
Step 3/8 : WORKDIR /app
 ---> Using cache
 ---> 4ac4194a3e8d
Step 4/8 : COPY entrypoint.sh entrypoint.sh
 ---> Using cache
 ---> 9dc7fedd1896
Step 5/8 : RUN chmod +x entrypoint.sh
 ---> Using cache
 ---> f84cba5af587
Step 6/8 : RUN echo "Hello World!"
 ---> Using cache
 ---> c31ffc29b1f2
Step 7/8 : RUN echo "Add echo"
 ---> Running in 066cb95b0f4e
Add echo
Removing intermediate container 066cb95b0f4e
 ---> 6023010b7f85
Step 8/8 : ENTRYPOINT ["./entrypoint.sh"]
 ---> Running in 8e143ade8c8c
Removing intermediate container 8e143ade8c8c
 ---> d38f012d9a2b
Successfully built d38f012d9a2b
Successfully tagged test:v1

real    0m1.630s
user    0m0.035s
sys     0m0.060s

※ちなみに、ビルド時のレイヤーキャッシュ利用とそれが無効化される場合に関しては、公式に以下のように記述がある。

If you have multiple Dockerfile steps that use different files from your context, COPY them individually, rather than all at once. This ensures that each step’s build cache is only invalidated (forcing the step to be re-run) if the specifically required files change.(コンテキストから異なるファイルを使用する複数の Dockerfile ステップがある場合、それらを一度に全てではなく、個別に COPY してください。これにより、各ステップのビルドキャッシュは、特に必要なファイルが変更された場合にのみ無効化される(ステップを再実行することを余儀なくされる)ことが保証されます。)

②Docker イメージを pull する際

Docker イメージをレジストリから pull する時、以下のようにイメージのレイヤーが同じであればそれが利用され、ローカルにない部分だけを差分としてダウンロードする動きが確認できる。
具体的には、v1 のイメージを pull した時にローカルにダウンロードされたレイヤーは、v2 の pull 時にはダウンロードされず、"Already exists"として再利用されている事が確認できる。

※ちなみに、docker push の時にもレイヤーを活かした動きになっており、以下のように v1 と v2 での差のみが、v2 の push 時に Pushed になっている事が確認できる。
つまり、v2 のイメージを push する際には、既に push 済みのもの以外だけ push され、データの転送量はその分だけ抑えられる。

効率のいい Dockerfile を書く

最後に、上記のレイヤーとそのキャッシュの事を踏まえ、前回の記事「マルチステージビルドで Docker イメージのサイズを削減してみた」で実装した Dockerfile を、より効率的な Dockerfile に変更してみたいと思う。

結論としては以下のように、node_modules をインストールする部分を別レイヤーになるようにし、依存関係が変更された時のみ再度 node_modules をインストールするようにすればいい。
こうすると、Docker イメージをビルドする際に時間がかかる node_modules のインストールを 1 つのレイヤーとしてキャッシュできるので、Docker イメージのビルド時間の短縮が見込める。

FROM node:16.19.0-alpine3.16 AS builder

RUN mkdir /app
WORKDIR /app

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

COPY . .
RUN chmod +x ./build/app/entrypoint.sh && \
    yarn build && \
    mkdir for-next-stage && \
    mv build config dist src static package.json yarn.lock for-next-stage/

# 省略

実際に上記の変更をした後で、イメージをビルドしてみると、変更前に比べて約 1 分 47 秒のビルド時間の削減効果があった(時間は3回試しての平均値です)。

上記のように、Docker のレイヤーとそのキャッシュを意識して Dockerfile を実装する事で、効率よくイメージをビルドできるだろう。

まとめとして

今回はより効率よく Docker イメージをビルドするために、Docker のレイヤーとキャッシュの動きを確認しつつ、実際に Dockerfile を変更してビルド時間の削減ができる事を確認した。
DevOps の観点からはビルドの時間を短くするというのはサービスの開発を加速する上で重要になるので、今回見たような工夫をしてビルド時間を短くできるといいのではと思った。

※ちなみに、今回はマルチステージビルドを利用しており、builder のイメージは捨てているので問題ないが、Minimize the number of layersにも書かれている通り、Docker のレイヤーは最小限にしないとイメージサイズの肥大化になるので注意も必要ではある。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/