Dockerfileを改善して、ビルド時間の短縮をためしてみた
はじめに
こんにちは、SHIFTの開発部門に所属している Katayama です。
マルチステージビルドで Docker イメージのサイズを削減してみたでは、マルチステージビルドで Docker イメージのサイズの削減をやってみた。
今回はその中で次回の記事で取り上げると書いていた「レイヤー・キャッシュ」について、実際にどのように Dockerfile を記述すると、レイヤーキャッシュを活用してビルド時間を短くできるか?を見ていきたいと思う。
Docker のレイヤー
Docker のレイヤーとは、以下の画像のように Dockerfile の COPY や RUN などの各コマンドで作成される層の事。
詳細はBest practices for writing Dockerfilesに書かれている。
このレイヤー、再利用可能であればそれを再利用するという仕組みになっており、レイヤーを意識する事で効率のいい 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
※ちなみに、ビルド時のレイヤーキャッシュ利用とそれが無効化される場合に関しては、公式に以下のように記述がある。
②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 のレイヤーは最小限にしないとイメージサイズの肥大化になるので注意も必要ではある。
《この公式ブロガーの記事一覧》
お問合せはお気軽に
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/