`RUN --mount=type=cache`のキャッシュをGitHub Actionsのキャッシュとして保存しイメージビルドを速くする

背景

CI環境でのDockerイメージビルドの速度向上は、多くの開発者にとって切実な課題です。

簡単な対策は

しかしこれらのキャッシュはCI側で有効期限があり、キャッシュ保存量制限もあり、CircleCIの場合はコスト(料金)もかかります。

なので次によく取られる対策は、キャッシュをイメージレジストリに保存するやり方になります。
参考: https://shepherdmaster.hateblo.jp/entry/2022/06/11/123419

これによりCI環境でもレイヤーごとのキャッシュは可能になりました。 しかしパッケージが変更された場合(例えばpackage.jsonやgo.modやGemfileが変更になった場合)はパッケージのインストールはフルで実行されてしまいます。 ローカルで実行するのであれば追加/変更したパッケージだけインストールすればすむのにCI環境では少しでも変更があると毎回フルインストールが走ってビルド時間が延びるのが悩みです。

さて、このイメージビルド時のパッケージのインストールにキャッシュを効かせて速くする方法があります。
RUN --mount=type=cacheです。
https://docs.docker.com/build/guide/mounts/
これはローカルマシンでは効果がありますが、同一マシンじゃないとキャッシュが効かないため、マシンが毎回変わるCIでは効果がありません。またキャッシュ保存先がDockerが管理するディレクトリに保存されるためキャッシュが取り出しづらいという問題があります。

reproducible-containers/buildkit-cache-danceという救世主

RUN --mount=type=cacheのキャッシュをGitHub Actions上で保存できるようにするためのActionがreproducible-containers/buildkit-cache-danceです。
これを使えば、apt-getやパッケージ(npm, Goライブラリ, Gem等)のインストール結果をキャッシュすることができるようになります。
このActionやってることはシンプル(だが泥臭い)で、

という流れになっています。

apt-getのキャッシュの仕方

https://github.com/reproducible-containers/buildkit-cache-dance/tree/main#examples を参考

bundle installのキャッシュの仕方

Dockerfile:

WORKDIR /app/  
COPY Gemfile Gemfile.lock ./  
RUN bundle config set path .cache/bundle  
RUN --mount=type=cache,sharing=locked,target=/app/.cache/bundle \  
    bundle install && \  
    cp -ar .cache/bundle .bundle && \
    bundle config set path .bundle  

Action:

jobs:  
  build:  
    name: container build  
    runs-on: ubuntu-latest  
  
    steps:  
      - uses: actions/checkout@v3  
  
      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  
  
      - name: Cache bundle-install  
        uses: actions/cache@v3  
        with:  
          path: bundle-install-cache  
          key: bundle-cache-${{ hashFiles('Dockerfile') }}  
  
      - name: inject bundle-install-cache into docker  
        uses: reproducible-containers/buildkit-cache-dance@v2.1.3  
        with:  
          cache-source: bundle-install-cache  
          cache-target: /app/.cache/bundle  
  
      - uses: docker/build-push-action@v5  
        with:  
          context: .  
          tags: user/app:latest

その他

試してないですが、npmやGoライブラリなどのキャッシュも同様に出来るはずです。

注意点

CI上でのキャッシュのrestore、save、そしてイメージへのキャッシュのinject、イメージからのキャッシュのextractはキャッシュサイズが大きくなればなるほど時間がかかるようになります。 そのためRUN --mount=type=cacheのキャッシュが効くことによる削減時間よりも先述したオーバーヘッドのほうが大きくなる場合があるので注意が必要です。

まとめ

RUN --mount=type=cacheのキャッシュをGitHub Actionsのキャッシュとして保存し、イメージビルドを速くする方法を紹介しました。 キャッシュのサイズによりますがDependabotやRenovateでパッケージのアップデートを頻繁に行う場合でもイメージビルドが速くなるのは嬉しいですね。