Dockerのビルドにかかるリードタイムを改善するためのTips3選

この記事は[DMMグループ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dmm)の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`を設定します。

```shell

DOCKER_BUILDKIT=1 docker build -t test .
```


## イメージサイズを計測する

[wagoodman/dive](https://github.com/wagoodman/dive)というDockerのイメージを解析するツールがあります。
これを使うことでレイヤーやファイルのサイズを確認できます。

diveはjson形式の出力に対応しています。
下記のようにjqを利用すると、レイヤーやファイルのサイズを降順にソートして表示できます。

diveの結果をファイルに保存

```shell
dive <IMAGE:TAG> --json <FILENAME>.json
```

サイズの大きいレイヤーを10件降順で表示

```shell
cat <FILENAME>.json  | jq '.layer | sort_by(.sizeBytes) | reverse | [limit(10; .[])]'
```


サイズの大きいファイルを10件降順で表示

```shell
cat <FILENAME>.json | jq '.image.fileReference | sort_by(.sizeBytes) | reverse | [limit(10; .[])]'
```

ビルド時間の計測とイメージサイズの確認をおこなうことで、問題箇所の特定に繋がりました。

# Tips2　BuildKitを使って並列でマルチステージビルドする

BuildKitはマルチステージビルド時に、依存関係の無いステージを並列で実行してくれます。
下記のようなDockerfileで実際に挙動を確認してみましょう。

```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の結果

```shell
$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の結果

```shell

$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](https://github.com/actions/cache)と組み合わせて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
実際のコードは以下のようになります。

```..github/workflows/build.yaml
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回め以降のビルドはキャッシュが効いてビルド時間が短くなることが確認できました。

c.f. https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows

# まとめ
今回

- Dockerイメージの計測
- マルチステージビルドの並列実行
- CI上でキャッシュの利用

について書きました。
改善を実施するときは、まずは計測から初めて、改善コストと対費用効果のコストパフォーマンスを考慮してから実際の改善をはじめましょう。
また、今回紹介した改善に取り掛かる前に、[Dockerfileのベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)に沿っているか確認しましょう。
それでもまだCIに時間がかかっていている場合は、今回ご紹介したTipsを検討してみてはいかがでしょうか。