GitHub ActionsでファイルをS3にキャッシュするアクションを作りました

GitHub Actionsでは依存パッケージやビルド結果などをうまくキャッシュすることで、テストやビルドの時間を短縮できます。 actions/setup-nodeactions/setup-javaなどの各言語のオフィシャルアクションは各パッケージマネージャーのためのキャッシュ機構を提供していますし、actions/cacheを使って任意のファイルをキャッシュすることもできます。 これらは内部で@actions/cacheパッケージを使っており、キャッシュの機構はGitHub自身の機能と密に結びついています。 しかし、GitHub Actionsのキャッシュはリポジトリごとに10GBまでという制限があり、開発者の多いリポジトリではsetup-nodeのキャッシュだけでもすぐに上限に達してしまいます。 私の所属するチームのリポジトリGitHub Enterprise Serverにホストされており、キャッシュの制限は25GBに緩和してもらっていますが (参考)、それでも一日に数十GB以上利用してしまう日もあり、効果的にキャッシュを利用できているとは言えません。

今回、GitHub ActionsでファイルをAmazon S3にキャッシュするアクションをフルスクラッチで作りました。 二週間前から作り始めてようやく形になってきたので、タグを打ってMarketplaceにも公開しました。

- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-region: ${{ vars.S3_CACHE_AWS_REGION }}
    role-to-assume: ${{ vars.S3_CACHE_ASSUME_ROLE_ARN }}
- uses: itchyny/s3-cache-action@v1
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-
    bucket-name: ${{ vars.S3_CACHE_BUCKET_NAME }}
    # AWSの認証情報を直に指定することも可能
    # aws-region: ${{ vars.S3_CACHE_AWS_REGION }}
    # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

基本的にactions/cacheとほぼ同じように使えますが、いくつかの違いがあります。 まず、ブランチによるスコープ分離がありません。 actions/cacheのこの制約 (参考) は安全だとは思いますが、私のチームでは同じキーのキャッシュが大量に作られることもあり、とても不便に感じています。 私のアクションにはブランチによるスコープは実装していません。 必要であればkeysrestore-keysにブランチ名を含めると良いでしょう。 また、actions/cacheWindowsとそれ以外のOSでキャッシュが混ざらないようになっていますが、同じことはkeyrunner.osを含めることで実現できるので、私のアクションでは実装していません。 そのためenableCrossOsArchiveというオプションはありません。

actions/cacheにはブランチのスコープとは別にバージョンという概念があります (参考)。 簡単に言うと、キャッシュのpathが異なるキャッシュは別のバージョンとして扱われます。 これは重要な機能で、単純にkeyだけでキャッシュをマッチさせてしまうとpathだけを変えた時に意図しないキャッシュをリストアしてしまいます。 キャッシュのバージョンがあるおかげで、pathに新しいディレクトリを追加したとしても新しいkeyを考えなくてもよくなっているのです。 私の作ったアクションでも、pathに基づいたハッシュをオブジェクトのキーに付与することで同じような挙動を実装しています。

actions/cachepathが違えば別のキャッシュとして扱われるというこの挙動は、実装を追っていくと納得できる挙動でもあります。 このアクションは、tarコマンドで--absolute-names (-P)オプションを使って絶対パスを含めたアーカイブにして保存しています。 展開時も同じオプションで展開するだけで、例えばpathに相当する場所に移動するという処理はありません。 そのため、仮にpathが一つ指定されているだけであったとしても、パスが違えば別のキャッシュとして扱われるのです。 actions/cache/saveで保存したファイルを別のパスにactions/cache/restoreできないのも、この実装によるものです。

今回、アクションを実装する前に既存のアクションが使えるかをかなり調査しましたが、自分のユースケースでまともに動きそうなアクションは一つも見つけられませんでした。 例えばS3にオブジェクトがたくさんある時にうまく動かなかったり、キャッシュのバージョンに相当する挙動を実装していなくてpathを変えてもリストアしてしまったりしました。 また、actions/cacheをforkしていたり、S3にアクセスできない場合にfallbackする機能を実装していたりして、私が欲しい物に対して実装が大きすぎて実装を追うのもつらく、メンテナンスも厳しそうに感じました (弊社ではVerified creatorでない作者のアクションを導入するにはソースコードの精査が義務付けられています)。 actions/cacheの中でも特に重要な機能を抽出しつつ、キャッシュをS3に保存するだけのシンプルなアクションが欲しかったので、自分で作ることにしました。

actions/cacheはネイティブのtarコマンドを実行していますが、s3-cache-actionnode-tarを使っています。 内部的にはnode-tarのtar部分はJavaScriptで書かれていて、gzipにはNode.jsのzlib bindingを使っています。 この実装で十分に速度が出ているので、特に問題はないと思っています。 Brotliも検証しましたが、npmパッケージの保存を試したところ圧縮処理がとても遅くなり、キャッシュサイズもgzipと大差なかったのでやめました。 globパターンの展開はactions/cacheと同じく@actions/globを使っているので、ここの挙動の際はありません。

GitHub Actionsでのキャッシュにお困りの方は、ぜひ使ってみてください。 それでは、また。 github.com