Chapter 04

リモートキャッシュ

Kesin11
Kesin11
2020.11.08に更新

https://docs.bazel.build/versions/master/remote-caching.html

いよいよ最後に、これまでたびたび紹介してきたリモートキャッシュについて解説します。Bazelにはこれまで解説してきたビルドやテスト結果のキャッシュをローカルファイルだけでなく、リモートから取得・保存する機能が組み込まれています。

リモートキャッシュはGoogleが専用のサービスを提供しているわけではなく、自前でキャッシュサーバーを建てるかGCSを利用することになります。GCSならば自前でサーバーを運用する必要がなくて個人でも手軽に始められるため、こちらを利用する方法を解説します。

https://docs.bazel.build/versions/master/remote-caching.html#google-cloud-storage

必要な作業は以下です。GCPの詳しい設定方法の説明は省きます。

  1. キャッシュ用のバケットを作る
    • ロケーションはネットワーク的に近いほうが良いでしょう
    • GCSのライフサイクル機能を設定して古いデータはN日で自動的に削除されるようにしておくと、古いキャッシュが自動的に削除されて容量の節約になります
  2. GCSにアクセスするためのJSON鍵を用意する
    • バケットに権限を付与したサービスアカウントのJSON鍵を用意するのが良い
    • 自分のマシンでしか使わないのならば個人アカウントのJSON鍵でも可能
    • CIサービスでもリモートキャッシュを使うことまで考えるとサービスアカウントを作成するほうが良い

リモートキャッシュを利用するためのオプションと.bazelrc

リモートキャッシュを使用するにはbazelコマンドに以下のようなオプションを追加します。

--remote_cache=https://storage.googleapis.com/{バケット名}
--google_credentials={JSON鍵のパス}

このオプションをpackage.jsonのnpm run buildなどに書いてしまうとバケット名やJSON鍵のパスを変更しにくくなってしまうため、.bazelrcを利用します。

以下のように~/.bazelrcを作成します。

build --remote_cache=https://storage.googleapis.com/{バケット名} --google_credentials={JSON鍵のパス}
test --remote_cache=https://storage.googleapis.com/{バケット名} --google_credentials={JSON鍵のパス}
run --remote_cache=https://storage.googleapis.com/{バケット名} --google_credentials={JSON鍵のパス}

.bazelrcは、行の先頭のコマンドに対してデフォルトで付与するオプションを書くことができます。自分のサンプルリポジトリではbuild、test、runの3つのbazelコマンドを使用しているので、この3つ全てにリモートキャッシュの設定を追加します。

ディスクキャッシュ

https://docs.bazel.build/versions/master/remote-caching.html#disk-cache

では実際に動作確認と行きたいところですが、その前にここまでのビルドでローカルに保存されてきたディスクキャッシュを削除しておきます。これまで使用してきたキャッシュはbazel cleanで削除できるものだけだと思いきや、実はそれ以外にも存在しているのです。

リモートキャッシュと同様に、複数のBazelプロジェクトのキャッシュをローカルで共有するためのディレクトリを—-disk_cacheで指定できます。実は2章で最初にBazelプロジェクトを作成するのに使用したnpm init @bazel {PROJECT_NAME}で作られたディレクトリの中には.bazelrcが初めから用意されており、その中でこのようにディスクキャッシュのパスが指定されています。

build --disk_cache=~/.cache/bazel-disk-cache

ドキュメントに明確に書かれていないので自分の経験と推測ですが、Bazelのキャッシュはこのような優先度になっています。

  1. プロジェクト固有のビルドキャッシュ
    • ~/.cache/bazel/ 以下に保存されている
    • bazel clean で削除される
  2. ディスクキャッシュ
    • —-disk_cache で指定したパス(~/.cache/bazel-disk-cacheなど)
    • 自動的に削除されることはない
  3. リモートキャッシュ
    • 指定したサーバーやGCSのバケット
    • 削除するかはサーバーの設定次第(GCSの場合はライフサイクルで自動削除も可能)

つまり、bazel cleanでキャッシュを消したとしてもディスクキャッシュが残っているうちはリモートキャッシュが使われることはないということです。さらにややこしいのが、ディスクキャッシュはBazelの中ではリモートキャッシュの1つという扱いのため、ログなどではリモートキャッシュにヒットしたと表示される点です。

まずはローカルのキャッシュを参照することでリモートへのアクセスを減らすという、キャッシュの戦略としては正しい仕組みなのですが、この後のリモートキャッシュの動作確認には混乱の元になるので一度ディスクキャッシュを削除しておきましょう。

rm -rf ~/.cache/bazel-disk-cache/

動作確認

実際に2章で使用したts_projectのサンプルリポジトリを使ってリモートキャッシュの動作確認をしてみましょう。

まずはbazel cleanでキャッシュを削除してから、.bazelrcにリモートキャッシュの設定がされている状態でビルドします。先ほどのディスクキャッシュ削除とあわせて、完全にキャッシュが存在しない状態でのビルドになります。

$ npm run clean
$ npm run test

> @ test /home/codespace/workspace/bazel-playground/ts_project
> bazel test //...

Killed non-responsive server process (pid=12473)
Starting local Bazel server and connecting to it...
INFO: Invocation ID: 1194c59d-d776-4eea-b17f-9823f0c7fb67
INFO: Analyzed 4 targets (480 packages loaded, 10334 targets configured).
INFO: Found 2 targets and 2 test targets...
INFO: Elapsed time: 142.379s, Critical Path: 32.79s
INFO: 6 processes: 6 processwrapper-sandbox.
INFO: Build completed successfully, 31 total actions
//:index_test                                                            PASSED in 0.8s
//:jest_test                                                             PASSED in 3.9s

Executed 2 out of 2 tests: 2 tests pass.
INFO: Build completed successfully, 31 total actions

では実際にリモートキャッシュが保存されているかを確認してみましょう。例ではgsutilを使いますが、GCPのコンソールからバケットの中身を確認する方が簡単です。

$ gsutil ls gs://{BUCKET}
gs://{BUCKET}/ac/
gs://{BUCKET}/cas/

$ ls gs://{BUCKET}/ac
gs://{BUCKET}/ac/1b45c99c7baf8c08f9180565397496b3ccbb37419ed2943cb2d69b5087b8a561
gs://{BUCKET}/ac/44ac017e343c4eeb92b1ddab617d1431c35a3e701f92c5dbb58dab9b81a32ab9
# 大量にファイルが表示されるので残りは省略

$ ls gs://{BUCKET}/cas
gs://{BUCKET}/cas/0f12f6749d4f54f683ec3a72f8ee0b82a29d3a33712d8459875760f353961d4e
gs://{BUCKET}/cas/1b45c99c7baf8c08f9180565397496b3ccbb37419ed2943cb2d69b5087b8a561
# 大量にファイルが表示されるので残りは省略

次にディスクキャッシュはどうなったか見てみましょう。

$ ls ~/.cache/bazel-disk-cache/
0f12f6749d4f54f683ec3a72f8ee0b82a29d3a33712d8459875760f353961d4e     ac_ce17a001b24d23dc619fc87f4d28c9c1fdcd6864a7da6c8df84c5e445d4e51a2
1b45c99c7baf8c08f9180565397496b3ccbb37419ed2943cb2d69b5087b8a561     ac_dae4da49a857b85cf44eaa7f8da507be88ffbfb68c849b5e1f642c076928883d
# 大量にファイルが表示されるので残りは省略

このようにリモートだけではなくディスクにもキャッシュが残されていることが分かります。

では次はローカルにキャッシュが存在していない状況でリモートキャッシュを使ってくれるかどうかを確認しましょう。

$ npm run clean
$ rm -rf ~/.cache/bazel-disk-cache/
$ npm run test

> @ test /home/codespace/workspace/bazel-playground/ts_project
> bazel test //...

INFO: Invocation ID: cf08cb5e-f6b9-4fbc-89fd-517dd87a3d15
INFO: Analyzed 4 targets (480 packages loaded, 10334 targets configured).
INFO: Found 2 targets and 2 test targets...
INFO: Elapsed time: 50.737s, Critical Path: 5.03s
INFO: 6 processes: 5 remote cache hit, 1 processwrapper-sandbox.
INFO: Build completed successfully, 31 total actions
//:index_test                                                   (cached) PASSED in 1.1s
//:jest_test                                                    (cached) PASSED in 1.1s

Executed 0 out of 2 tests: 2 tests pass.
INFO: Build completed successfully, 31 total actions

このようにログからリモートキャッシュを使用したことが分かります。

ちなみに、一度リモートキャッシュが正しく利用できていることが確認できれば今後はディスクキャッシュも併用する方がパフォーマンス的に有利なのですが、リモートキャッシュの動作確認をしているときには毎回~/.cache/bazel-disk-cacheを削除するのは面倒くさいと思います。その場合、—-disk_cache=をオプションに指定することでディスクキャッシュを使用しなくなります。

--no-disk_cacheみたいな分かりやすいオプションは存在していないのですが、パスを指定しないことでディスクキャッシュの機能をオフにできるようです(ドキュメントには書いていない)。

https://github.com/bazelbuild/bazel/issues/5308

CIでもリモートキャッシュを使用する

最初の1章でも紹介したように、リモートキャッシュを利用する利点としてCI環境で生成されたビルドキャッシュを利用することでブランチを切り替えた場合などでもキャッシュにヒットしやすくなります。

そのためにはCIでもリモートキャッシュを利用するように設定する必要がありますが、サービスアカウントのJSON鍵をCI環境に持ち込み、.bazelrcを設定するだけで特に難しくないので割愛します。

興味がある方はサンプルリポジトリでGitHub Actionsを利用したCIのコードを公開しているのでそちらを参考にしてください。

https://github.com/Kesin11/bazel-playground/blob/master/.github/workflows/main.yml

4章のまとめ

4章ではBazelの強力なリモートキャッシュを利用する方法を解説しました。3章まではサンプルコードをcloneして実行するだけで動くはずですが、リモートキャッシュに関しては各自でGCSを用意してもらう必要があるので手元で再現してもらうのは少し大変だと思います。ですが、特にチーム開発ではリモートキャッシュを使うことでクリーンビルドやブランチを切り替えたときのビルドが圧倒的に早くなることが期待できるので利用できる大きなメリットがあるはずです。