この記事はDMMグループ Advent Calendar 2020の19日目の記事です。
概要
Dockerのビルドにかかるリードタイムを縮めるための改善を3つおこなったのでTipsをまとめます。
環境
- GitHub Actions
- Node.js 14.15.1
- Docker-Buildx 0.4.2
- Docker-Moby 19.03.13
今回、GitHub Actionsでubuntu-18.04を利用しており、ツールのバージョンは下記にまとめられています。
c.f. https://github.com/actions/virtual-environments
解析用にローカルでjqを利用します。
- jq 1.6
Tips1 計測する
ビルド時間を計測する
BuildKitを利用してビルド時間を計測します。
BuildKitとはDocker18.09以降に組み込まれているツールで、
依存関係の並行解決や、キャッシュのインポート・エクスポートができます。
c.f. https://github.com/moby/buildkit
BuildKitを使ってビルドすると、ビルドにかかった時間が表示されます。
利用するには環境変数にDOCKER_BUILDKIT=1
を設定します。
DOCKER_BUILDKIT=1 docker build -t test .
イメージサイズを計測する
wagoodman/diveというDockerのイメージを解析するツールがあります。
これを使うことでレイヤーやファイルのサイズを確認できます。
diveはjson形式の出力に対応しています。
下記のようにjqを利用すると、レイヤーやファイルのサイズを降順にソートして表示できます。
diveの結果をファイルに保存
dive <IMAGE:TAG> --json <FILENAME>.json
サイズの大きいレイヤーを10件降順で表示
cat <FILENAME>.json | jq '.layer | sort_by(.sizeBytes) | reverse | [limit(10; .[])]'
サイズの大きいファイルを10件降順で表示
cat <FILENAME>.json | jq '.image.fileReference | sort_by(.sizeBytes) | reverse | [limit(10; .[])]'
ビルド時間の計測とイメージサイズの確認をおこなうことで、問題箇所の特定に繋がりました。
Tips2 BuildKitを使って並列でマルチステージビルドする
BuildKitはマルチステージビルド時に、依存関係の無いステージを並列で実行してくれます。
下記のようなDockerfileで実際に挙動を確認してみましょう。
FROM alpine AS stage1
RUN echo "stage1" \
&& sleep 5 \
&& echo "stage1" > stage1.txt
FROM alpine AS stage2
RUN echo "stage2" \
&& sleep 5 \
&& echo "stage2" > stage2.txt
FROM alpine
COPY --from=stage1 /stage1.txt ./
COPY --from=stage2 /stage2.txt ./
通常のDocker buildの結果
$time docker build -t test . --no-cache
Sending build context to Docker daemon 8.192kB
Step 1/7 : FROM alpine AS stage1
---> 389fef711851
Step 2/7 : RUN echo "stage1" && sleep 5 && echo "stage1" > stage1.txt
---> Running in 060af4f159d1
stage1
Removing intermediate container 060af4f159d1
---> 47cacbebce55
Step 3/7 : FROM alpine AS stage2
---> 389fef711851
Step 4/7 : RUN echo "stage2" && sleep 5 && echo "stage2" > stage2.txt
---> Running in 5527e0adf01c
stage2
Removing intermediate container 5527e0adf01c
---> c4c36b1aaa7b
Step 5/7 : FROM alpine
---> 389fef711851
Step 6/7 : COPY --from=stage1 /stage1.txt ./
---> b29d3db1464c
Step 7/7 : COPY --from=stage2 /stage2.txt ./
---> 2ae9b56c1d34
Successfully built 2ae9b56c1d34
Successfully tagged test:latest
real 0m11.976s
user 0m0.261s
sys 0m0.190s
stage1とstage2でそれぞれ5秒間のsleepコマンドを実行しているので、10秒近くかかることが確認できます。
BuildKitの結果
$DOCKER_BUILDKIT=1 docker build -t test . --no-cache
[+] Building 5.6s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 322B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
=> [stage2 1/2] FROM docker.io/library/alpine 0.0s
=> => resolve docker.io/library/alpine:latest 0.0s
=> [stage2 2/2] RUN echo "stage2" && sleep 5 && echo "stage2" > stage2.txt 5.3s
=> [stage1 2/2] RUN echo "stage1" && sleep 5 && echo "stage1" > stage1.txt 5.3s
=> [runner 2/3] COPY --from=stage1 /stage1.txt ./ 0.1s
=> [runner 3/3] COPY --from=stage2 /stage2.txt ./ 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:27621000a30c0338903150a187a8b665a56137b5d14c018d0ac083a716d2fb72 0.0s
=> => naming to docker.io/library/test 0.0s
このようにstage1とstage2が並列実行されて5秒程度しかかからないことが確認できます。
Tips3 CIでcacheを利用する
Docker公式のビルド、プッシュ用のGitHub Actionsがあります。
https://github.com/docker/build-push-action
バージョン2以降はBuildxを利用してイメージのビルドとプッシュを行います。
これをactions/cache@v2と組み合わせてCI上でキャッシュを効かせます。
buildxとは
buildxとはBuildKitの機能をすべてサポートしているCLIプラグインです。
ただし、2020年12月時点では実験的機能であり、本番環境利用が推奨されていません。
c.f. https://docs.docker.com/buildx/working-with-buildx/
c.f. https://github.com/docker/buildx
中間ステージのキャッシュについて
マルチステージビルドを利用していて、中間ステージも含めてキャッシュしたいときは--chace-to
オプションにmode=max
を記述します。
mode=min
とすると最終的にビルドされたステージのみキャッシュします。
キャッシュのタイプについて
BuildKitは下記の3つのタイプのキャッシュがあります。
今回はlocalタイプを利用しましたが、実用のためにはregistryキャッシュを使いましょう。
(記事執筆時点ではregistryキャッシュがECRに対応しておらず公式リポジトリにいくつかISSUEが上がっていたため採用を見送りました。)
- inline:キャッシュをDockerイメージに埋め込む
- registry:イメージとキャッシュを別々にプッシュする
- local:ローカルディレクトリにキャッシュをエクスポートする
Example
実際のコードは以下のようになります。
name: Build and Push Container
on:
push:
branches:
- 'main'
jobs:
build-push-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and Push
uses: docker/build-push-action@v2
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ github.repository }}
with:
push: true
tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
build-args: |
ARG1=hoge
actions/cache@v2
でキャッシュのパスを指定し、それをdocker/build-push-action@v2
で利用します。
これにより2回め以降のビルドはキャッシュが効いてビルド時間が短くなることが確認できました。
まとめ
今回
- Dockerイメージの計測
- マルチステージビルドの並列実行
- CI上でキャッシュの利用
について書きました。
改善を実施するときは、まずは計測から初めて、改善コストと対費用効果のコストパフォーマンスを考慮してから実際の改善をはじめましょう。
また、今回紹介した改善に取り掛かる前に、Dockerfileのベストプラクティスに沿っているか確認しましょう。
それでもまだCIに時間がかかっていている場合は、今回ご紹介したTipsを検討してみてはいかがでしょうか。