Gradle依存ファイルをcacheしてdocker buildを高速化する


要旨

  • Gradle関係ファイルのみ COPY した状態で gradlew build を行うことで、依存ファイルをダウンロードしたキャッシュ用レイヤを作る
  • Docker imageのビルド時間を約70%削減した(※環境・プロジェクトにより差はあります。)

動機

Spring BootアプリケーションのDocker imageのビルド時間が5分以上掛かっていました。
このため、検品環境へのビルド・デプロイに時間がかかってしまい、
検品環境でしかできない試行錯誤に時間がかかるという課題がありました。

Sample Dockerfile

FROM openjdk:11.0.8-jre-slim as builder

WORKDIR /app

# build.gradleに依存するファイルのみを追加し、
# gradle buildを空回しして依存ファイルをキャッシュする
COPY *.gradle gradle.* gradlew /app/
COPY gradle/ /app/gradle/
RUN ./gradlew build -x test --parallel --continue > /dev/null 2>&1 || true

# 以下の行以降でコードの変更を取り込みビルドする
COPY . /app
RUN ./gradlew build -x test --parallel

FROM openjdk:11.0.8-jre-slim

COPY --from=builder /app/build/libs/app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]

Tips: multi-stage builds を利用し、ビルド用コンテナと実行用コンテナを分けています。

詳解

一般的なDockerfile例

よくあるGradle buildをするDockerfileは下記の様になると思います。

FROM openjdk:11.0.8-jre-slim as builder
WORKDIR /app
COPY . /app
RUN ./gradlew build -x test --parallel

このDockerfileの課題としては、ビルドのたびにgradleのバイナリや依存ファイルのダウンロードが行われるため、ビルドに時間がかかってしまいます。
今回はこのgradleのバイナリと依存ファイルのダウンロードをイメージのレイヤにキャッシュすることで高速化を行います。

Gradleに依存ファイルをダウンロードするコマンドは無い

残念ながらGradleのコマンドにはバイナリや依存ファイルのダウンロードのみを実行するコマンドはありません。
しかしながらダウンロードはビルド実行時の冒頭で行われます。
そこでビルドが失敗する状態でもビルドを空回しすることで事前ダウンロードを行います。

$ ./gradlew build --continue > /dev/null 2>&1 || true

--continue のオプションを付けることで複数プロジェクトがあるビルドでも最後まで実行し、依存ファイルをダウンロードをすることができます。

COPY コマンドのキャッシュ特性

COPYコマンドではファイルのチェックサムを元にキャッシュ利用の可否を検査します。
(※最終更新日やアクセス日時ではありません)
そこで gradle buildに最低限必要なファイルを COPY し、 buildを空回しすることで依存ファイルをダウンロードしたレイヤを作ることができます。
build.gradle 等は変更されることが少ないため、多くのビルド時にキャッシュを利用することができビルド時間の削減が見込めます。

COPY *.gradle gradle.* gradlew /app/
COPY gradle/ /app/gradle/

削減効果

以上のGradle依存ファイルのキャッシュ利用をすることでDockerのイメージビルド時間を削減できると思います。
削減効果に関して、環境やJavaプロジェクトによって異なりますが、私の適用結果を共有しておきます。

適用前: 4min 56sec
適用後: 1min 42sec

今回の手法の適用によって約70%のビルド時間を削減することができました。

最後に

以上の手法で、少しでも世界のGradleを利用したDockerのイメージビルド時間が削減されれば幸いです。記述に間違い等がありましたら、コメントにてお知らせください。