GitHub Actions/Cache : Dockerコンテナ上でbundle installしたgemをキャッシュする

GitHub AcitonsでRailsプロジェクトのrspec/rubocopを実行するような場合に、`bundle install` したgemのキャッシュを行いたいのだが、`Permission denied`エラーが出てうまくキャッシュが作れず、キャッシュキーが汚染[^1]されることがあった。

```log:error例
Post job cleanup.
/bin/tar --posix --use-compress-program zstd -T0 -cf cache.tzst -P -C /home/runner/work/project/project --files-from manifest.txt
/bin/tar: ../../../../../tmp/cache/bundle/gems/puma-5.1.1/ext/puma_http11/.gem.20201227-1-wtkvl5: Cannot open: Permission denied
/bin/tar: ../../../../../tmp/cache/bundle/gems/mini_racer-0.3.1/ext/mini_racer_extension/.gem.
[中略]
/bin/tar: ../../../../../tmp/cache/bundle/gems/sassc-2.4.0/ext/.gem.20201227-1-1ompol5: Cannot open: Permission denied
/bin/tar: Exiting with failure status due to previous errors
Warning: Tar failed with error: The process '/bin/tar' failed with exit code 2
```

上記のエラーはローカルの `/var/cache/bundle` ディレクトリを、コンテナの `/bundle` にマウントした状態で `BUNDLE_PATH=/bundle bundle install` を実行し、`/var/cache/bundle` を `Actions/Cache` でキャッシュさせる場合に発生した

docker-compose と workflow のyamlは下記のような感じ

```yaml:docker-compose.yaml
version: '3.7'

services:
  app:
    ...

    volumes:
      - ./:/app:cached
      - /var/cache/bundle:/bundle
```

```yaml:.github/workflows/ci.yml
jobs:
  ...
  rspec:
    ...
    env:
      GEMS_CACHE_DIR: /tmp/cache/bundle

    steps:
    ...

    - name: Cache bundle gems
      id: cache-bundle-gems
      uses: actions/cache@v2
      with:
        path: ${{env.GEMS_CACHE_DIR}}
        key: ${{ runner.os }}-${{ matrix.ruby }}-${{ env.GEM_CACHE_TAG }}-${{ hashFiles('Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-${{ matrix.ruby }}-${{ env.GEM_CACHE_TAG }}-
```

## 解決策 1 アクセス権限を書き換える

アクセス権限の問題なので、雑にアクセス権限を書き換えてしまう方法。

cache作成時のtarコマンドでsudoがつけられたらいいのだが、そういうオプションはないので、下記のようなアクション`steps`の最後に差し込む

```yaml:.github/workflows/ci.yml
  - name: Fix permissions of bundle
    id: fix-permissions-of-bundle
    run: sudo chmod -R a+rwx ${{env.GEMS_CACHE_DIR}}
```

この方法で多分大丈夫。

ですが、アクセス権限書き換えることになるので、その辺りの動作確認は取れません（そこが問題になることなんてほぼないと思いますが…

## 解決策 2 コンテナ上でtarを作成してcacheさせる

アクセス権限の問題が発生するのは、コンテナ上で `bundler install` しているのにローカルでtarを作成しているため。

コンテナ上で `/bundle` のtarを作成し、そのtarを `Actions/Cache` でキャッシュして貰えば問題ありません。

下記の設定では必要最低限の軽量コンテナのみを立ち上げて（今回は `standalone` という名前のコンテナを使用している）、tarを作っています。

```yaml:.github/workflows/ci.yml
  env:
    GEMS_ARCHIVE_DIR: /tmp/cache/bundle-gem-archive
  ...
  # $GEMS_ARCHIVE_DIR ディレクトリをキャッシュ
  - name: Cache bundle gems
    id: cache-bundle-gems
    uses: actions/cache@v2
    with:
      path: ${{ env.GEMS_ARCHIVE_DIR }}
      key: ${{ runner.os }}-${{ matrix.ruby }}-${{ env.GEM_CACHE_TAG }}-${{ hashFiles('Gemfile.lock') }}
      restore-keys: |
        ${{ runner.os }}-${{ matrix.ruby }}-${{ env.GEM_CACHE_TAG }}-
    
  # $GEMS_ARCHIVE_DIR をマウントした軽量コンテナ上で /archive/bundle.tar からファイルを抽出
  - name: Unarchive bundle gems cache
    id: unarchive-bundle-gems-cache
    run: test -f ${{env.GEMS_ARCHIVE_DIR}}/bundle.tar
      && docker-compose run --rm -v ${{ env.GEMS_ARCHIVE_DIR }}:/archive:cached standalone
        tar -xf /archive/bundle.tar -C /
      || echo "bundle gem cache does not exist."
    
  ...

  # 軽量コンテナ上で $GEM_HOME の内容をtarで固め、/archive/bundle.tar の名前で保存
  - name: Create bundle gems archive
    id: create-bundle-gems-archive
    if: steps.cache-bundle-gems.outputs.cache-hit != 'true'
    run: docker-compose run --rm -v ${{ env.GEMS_ARCHIVE_DIR }}:/archive:cached
        standalone tar -cf /archive/bundle.tar -C / bundle
```

この方法の場合、コンテナ上でtarを実行するので多少遅くなるようだが自分の環境の場合はほぼ誤差（数秒程度）しか変わらなかった（gemの量などにもよると思う）。



[^1]: [GitHub Actions/Cache "Unable to reserve cache with key \{KEY\}, another job may be creating this cache\." エラー \- Qiita](https://qiita.com/HAZI/items/93c736bd55eef270421c)