GitHub Action 上で docker layer をキャッシュする

GitHub Action を CI やビルドツールとして使うとき,手元と動作を完結にさせたいので docker で済ませたいことがある. コンテナリポジトリにある docker image から直接 run できる場合は pull するだけで良いが, Dockerfile を別途用意する場合は image の取得に加えてビルドが必要になり,毎回時間がかかる.

例えば,circleci を使用する場合は,docker layer をキャッシュするオプションを追加するだけで良いが, GitHub Action には(現状)そういった使い勝手の良いキャッシュは無い.

circleci.com

代わりに任意のパス以下をキャッシュしてくれる組み込みの機能が存在している.

github.com

ただ,この actions/cache はまだまだ機能が不十分で,docker layer のキャッシュには工夫が必要だったのでこの記事はその対策を書く. 記事公開時点での対策であるため,公式なサポート等が出た場合はそれを利用することを推奨する.

actions/cache の制約

指定したパス以下をなんでもキャッシュしてくれるわけではなく,使用にあたっていくつかの制約が存在する.

  • パスごとの最大サイズは 400MB
  • リポジトリごとに最大のサイズが 2GB
    • 2GB を超える場合は古いキャッシュから順に削除される

2GB は仕方ないとして,docker layer のキャッシュとしてディレクトリごとに 400MB は制約が結構厳しい.

対策: 複数のパス以下に分散して配置する

あまり難しいことを考えずに,docker history コマンドから layer のハッシュ値を持ってきて,それを docker save で書き出す. もし 400MB を超える場合があるならば複数ファイルへ分割して配置する.

以下はサンプル.cache の複数パス指定は現段階ではサポート外であるため*1, 残念ながら冗長に必要な分だけ配置する必要がある. サンプルは2分割であるため冗長に書いているが,必要に応じて分割数を指定できるスクリプト等を用意したほうが良いかもしれない.

name: Build and Run with cache
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - uses: actions/cache@v1
      name: Splitted cache 01
      id: cache01
      with:
        path: docker-cache/01
        key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}-01
        restore-keys: ${{ runner.os }}-docker-01

    - uses: actions/cache@v1
      name: Splitted cache 02
      id: cache02
      with:
        path: docker-cache/02
        key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}-02
        restore-keys: ${{ runner.os }}-docker-02

    - name: Load cached docker layers
      if: steps.cache01.outputs.cache-hit == 'true'
      run: |
          cat docker-cache/*/file > layer-cache-sample.tar
          docker load < layer-cache-sample.tar

    - name: Build docker image with cache
      if: steps.cache01.outputs.cache-hit != 'true'
      run: |
        docker build --file Dockerfile --cache-from layer-cache-sample --tag layer-cache-sample .
        docker save layer-cache-sample $(docker history -q layer-cache-sample | awk '!/<missing>/{print}') > layer-cache-sample.tar
        rm -rf docker-cache && mkdir -p docker-cache/01 docker-cache/02
        split -n l/1/2 layer-cache-sample.tar > docker-cache/01/file
        split -n l/2/2 layer-cache-sample.tar > docker-cache/02/file

    - name: dokcer run
      run: docker run --rm layer-cache-sample:latest echo "Hello World!"

補足

docker layer のキャッシュについては以下の PR でサンプルについて議論されていて(記事執筆時点ではまだ議論中), 今回はその内容を参考にしつつ書いた.

github.com