cockscomblog?

cockscomb on hatena blog

GitHub ActionsでDocker Buildするときのキャッシュテクニック

GitHub Actionsでdocker buildすることが多い。このときのキャッシュをどうするかという話題。

基本

GitHub Actionsでdocker buildしてAmazon ECRにdocker pushする、典型的な.github/workflow/docker-push-to-ecr.ymlはこういう感じ。

name: Push to Amazon ECR

on:
  push:
    branches: [ 'main' ]

jobs:

  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v1

      - 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

      - uses: docker/metadata-action@v3
        id: meta
        with:
          images: |
            ${{ steps.login-ecr.outputs.registry }}/awesome-app 
          tags: |
            type=sha,prefix=

      - uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Docker周りはDocker公式のアクションを組み合わせて使って、BuildKitでやる。

キャッシュはdocker/build-push-action@v2cache-fromcache-toでやっていて、type=ghaとしてGitHub Actionsのキャッシュにのせている。

type=ghaは、BuildKitで使えるキャッシュの中でも手軽で使いやすい。GitHub Actionsのキャッシュはブランチと紐づいていて、現在のブランチとベースブランチのキャッシュが利用できるので、CIと組み合わせたときにわりと都合がいい。

Build Matrixとの組み合わせ

背景

去年、業務で利用しているMacがM1 Proのそれになって、我らがCTO id:motemenが用意しているmod_perl用Dockerイメージ(motemen/docker-mod_perl)にパッチを送り、arm64アーキテクチャのイメージをビルドしてもらうようにした。

変更は簡単で、docker/setup-qemu-actionQEMUのセットアップをして、docker/build-push-actionplatformsを指定した。ベースイメージの時点でマルチアーキテクチャに対応しているし、Dockerfileでアーキテクチャに依存したことをしていないので、これで十分。

マルチアーキテクチャになったのはよかったけど、GitHub Actionsがすごく遅くなってしまった。もともとキャッシュが切れていても5分30秒くらいで済んでいたのが、1時間以上かかるようになった。とはいえ、QEMUでCPUアーキテクチャをまたいでビルドすると遅いのは、それはそう。それはそうなんだけど、キャッシュの効きが悪いのも気になっていた。

f:id:cockscomb:20220215180147p:plain

add perl 5.34.0 · motemen/docker-mod_perl@1b6041d

こういうのをみると、Matrixでビルドしているうち「5.30.1」のみ9秒で終わっているが、残りは1時間近くかかっている。

f:id:cockscomb:20220215180222p:plain

Merge pull request #15 from motemen/modperl-2-0-12 · motemen/docker-mod_perl@ca11e85

さっきのブランチのベースになっているのはこちらで、「5.30.1」が一番最後に終わっていそうだった。

このことから、Matrixビルドでキャッシュが競合していて、後勝ちになっていることが類推された。

scopeを設定する

ということでBuildKitのghaキャッシュのドキュメントをもう一回見ると、scopeというのがある。これを使って、docker/build-push-action@v2cache-fromcache-toscopeを設定してみる。

          cache-from: type=gha,scope=perl-${{ matrix.perl_version }}
          cache-to: type=gha,mode=max,scope=perl-${{ matrix.perl_version }}

これでMatrixごとにキャッシュが分離された。

f:id:cockscomb:20220215175945p:plain

[DNM] test actions · motemen/docker-mod_perl@3b407c8

各バージョンが20秒前後になり、全体で1分弱になった。よかった。

まとめ

GitHub ActionsでBuildKitを使うときにtype=ghaのキャッシュが使える。ただしMatrixビルドなどで並列に複数のジョブが走ると、キャッシュが衝突してしまうが、scopeを設定することで回避できる。