Buildkitを使ってMulti-stage BuildでMavenのキャッシュを効かせる

# 背景

Dockerを利用する際、Multi-stage Buildが使われることが一般的になってきました。
しかし、Multi-stage Buildには、Mavenなどのパッケージマネージャのキャッシュを利用できないという欠点がありました。

そんな中、Dockerに搭載されたBuildkitで `--mount=type=cache` を使えば、Multi-stage Buildでもパッケージマネージャのキャッシュが利用できるようになったとのことです。

そこで、MavenプロジェクトのSpring BootをMulti-stage Build + Buildkitでdocker buildしてみました。

# 環境
- Docker for Mac
    - Docker Engine 18.09.0

# 手順

## Spring Bootプロジェクト作成
[Spring Initializr](https://start.spring.io/) でMavenプロジェクトのSpring Bootを初期化しました。

## Dockerfileを記述

Multi-stage Buildを使ったDockerfileを作成しました。

```dockerfile
# syntax = docker/dockerfile:1.0-experimental

FROM openjdk:8u181-jdk-alpine3.8 AS builder

ADD . /work
WORKDIR /work

RUN --mount=type=cache,target=/root/.m2 \
    ./mvnw clean install


FROM openjdk:8u181-jre-alpine3.8 AS runner

ENV JAR=demo-0.0.1-SNAPSHOT.jar

RUN addgroup -S -g 1000 app \
    && adduser -D -S -G app -u 1000 app \
    && apk add --no-cache su-exec
WORKDIR /home/app

COPY --from=builder --chown=app:app /work/target/$JAR /home/app/

CMD ["sh", "-c", "su-exec app java -jar $JAR"]
```

`# syntax = docker/dockerfile:1.0-experimental` の部分で `--mount=type=cache` という文法を有効化しています。

`RUN --mount=type=cache,target=/root/.m2` で.m2のキャッシュが効くようにしています。

## Buildkit有効化

```bash
$ export DOCKER_BUILDKIT=1
```

## ビルド1回目 [^1]
[^1]: 1回目と言いつつ実際には事前に何度か試しているため、ベースイメージなどのキャッシュが効いてしまっている部分があります。

```bash
$ docker build --no-cache -t demo .
[+] Building 151.6s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                               0.0s
 => => transferring dockerfile: 557B                                               0.0s
 => [internal] load .dockerignore                                                  0.0s
 => => transferring context: 2B                                                    0.0s
 => resolve image config for docker.io/docker/dockerfile:1.0-experimental          2.0s
 => CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d  0.0s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8     0.6s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8     0.8s
 => CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256  0.0s
 => [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e4  9.7s
 => => resolve docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e45570b6f5  0.0s
 => => sha256:ef87ded15917facc562d84e802d02ded848ae802cc0419440 70.61MB / 70.61MB  7.5s
 => => sha256:b18e45570b6f59bf80c15c78d7f0daff1e18e9c19069c323613 2.03kB / 2.03kB  0.0s
 => => sha256:3ef5b11bf99dc3829740e59e17dfaa491aa745816828286c6e0bf53 947B / 947B  0.0s
 => => sha256:97bc1352afdef16cbb71b5fe50c605e3174ba40c721ce9e0da3 3.40kB / 3.40kB  0.0s
 => => extracting sha256:ef87ded15917facc562d84e802d02ded848ae802cc04194400ba4cb8  1.9s
 => [internal] load build context                                                  0.1s
 => => transferring context: 67.98kB                                               0.0s
 => [runner 2/3] RUN addgroup -S -g 1000 app     && adduser -D -S -G app -u 1000   3.3s
 => CACHED [internal] helper image for file operations                             0.0s
 => [builder 2/3] ADD . /work                                                      0.5s
 => [builder 3/3] RUN --mount=type=cache,target=/root/.m2     ./mvnw clean inst  134.3s
 => [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP  1.9s
 => exporting to image                                                             0.1s
 => => exporting layers                                                            0.1s
 => => writing image sha256:d1628e99aa58995cf80c0f0e655dc136ea48953a3c75a2028088e  0.0s
 => => naming to docker.io/library/demo                                            0.0s
```

ビルドには151秒かかりました。

## ビルド2回目

```bash
$ docker build --no-cache -t demo .
[+] Building 24.4s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                               0.1s
 => => transferring dockerfile: 37B                                                0.0s
 => [internal] load .dockerignore                                                  0.0s
 => => transferring context: 2B                                                    0.0s
 => resolve image config for docker.io/docker/dockerfile:1.0-experimental          1.9s
 => CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d  0.0s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8     0.7s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8     0.8s
 => [internal] load build context                                                  0.0s
 => => transferring context: 1.67kB                                                0.0s
 => CACHED [internal] helper image for file operations                             0.0s
 => CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256  0.0s
 => CACHED [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha25  0.0s
 => [runner 2/3] RUN addgroup -S -g 1000 app     && adduser -D -S -G app -u 1000   2.6s
 => [builder 2/3] ADD . /work                                                      0.5s
 => [builder 3/3] RUN --mount=type=cache,target=/root/.m2     ./mvnw clean insta  17.6s
 => [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP  1.6s
 => exporting to image                                                             0.1s
 => => exporting layers                                                            0.1s
 => => writing image sha256:56669cdad5c5798018d9faa7598b9a63d93927b61f8b97c679c03  0.0s
 => => naming to docker.io/library/demo                                            0.0s
```

.m2のキャッシュが効いているようで、24秒でビルドできました。

# おまけ

## キャッシュ削除

キャッシュの削除方法は [INTRODUCING DOCKER ENGINE 18.09](https://blog.docker.com/2018/11/introducing-docker-engine-18-09/) に書かれていました。

> **Build cache pruning and configurable garbage collection:** Build cache can be managed separately from images and cleaned up with a new command `docker builder prune`. You can also set policies around when to clear build caches.

`docker builder prune` という新しいコマンドで削除できるとのことです。[^2]
[^2]: `docker system prune` でもキャッシュは削除されました。


実際に削除してみました。

```bash
$ docker builder prune
WARNING! This will remove all dangling build cache. Are you sure you want to continue? [y/N] y
Deleted build cache objects:
uz11u06qximcevxlom5o0oqjr
81lx4qivrfshyovit8f0tjcfk
mgin0p02pfn3o43rvkjo4ivz1
mh2e2399h35v8nzp2u3ifzmc3
ltd1751lktom5ns0zqkgrog8n
bhvsubwkwkz11gysean5ylce1
i3fpig7npayhyh5t7gkfvtooe
abp0xlrlny8prxyl9bs90durc
sha256:0a3e38ed053dfc9a81a4f7cc87c7b1588c436663f726f013937491ab42fc2931

Total reclaimed space: 179.2MB
```

## ビルド3回目

```bash
$ docker build --no-cache -t demo .
[+] Building 143.7s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                               0.1s
 => => transferring dockerfile: 557B                                               0.0s
 => [internal] load .dockerignore                                                  0.0s
 => => transferring context: 2B                                                    0.0s
 => resolve image config for docker.io/docker/dockerfile:1.0-experimental          0.8s
 => CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d  0.0s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8     1.8s
 => [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8     1.2s
 => CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256  0.0s
 => CACHED [internal] helper image for file operations                             0.0s
 => [internal] load build context                                                  0.1s
 => => transferring context: 67.98kB                                               0.0s
 => [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e  10.1s
 => => resolve docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e45570b6f5  0.0s
 => => sha256:ef87ded15917facc562d84e802d02ded848ae802cc0419440 70.61MB / 70.61MB  7.6s
 => => sha256:b18e45570b6f59bf80c15c78d7f0daff1e18e9c19069c323613 2.03kB / 2.03kB  0.0s
 => => sha256:3ef5b11bf99dc3829740e59e17dfaa491aa745816828286c6e0bf53 947B / 947B  0.0s
 => => sha256:97bc1352afdef16cbb71b5fe50c605e3174ba40c721ce9e0da3 3.40kB / 3.40kB  0.0s
 => => extracting sha256:ef87ded15917facc562d84e802d02ded848ae802cc04194400ba4cb8  2.2s
 => [runner 2/3] RUN addgroup -S -g 1000 app     && adduser -D -S -G app -u 1000   2.8s
 => [builder 2/3] ADD . /work                                                      0.5s
 => [builder 3/3] RUN --mount=type=cache,target=/root/.m2     ./mvnw clean inst  126.6s
 => [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP  1.7s
 => exporting to image                                                             0.2s
 => => exporting layers                                                            0.1s
 => => writing image sha256:d832ff1987e91375bf080b69504233eafe717d2c46043a7179e9e  0.0s
 => => naming to docker.io/library/demo                                            0.0s
```

キャッシュを削除すると、143秒という1回目と同程度の時間がかかりました。

# まとめ

Buildkitで `--mount=type=cache` を使うことで、Multi-stage buildでパッケージマネージャのキャッシュを利用できました。
同様にして、Maven以外のキャッシュも使えるはずです。

# 補足
CircleCIなどCIのSaaSを利用する場合、CI側のキャッシュ設定も必要なので注意してください。

# 参考
- [Dockerfile frontend experimental syntaxes](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md)
- [Build Enhancements for Docker](https://docs.docker.com/develop/develop-images/build_enhancements/#requirements)
- [Docker 18.09 新機能](https://www.slideshare.net/AkihiroSuda/docker-1809)
- [JavaでもDockerでマルチステージビルド](https://qiita.com/shibukawa/items/bd953853e41f70b2e1d7)