Bazel の Remote caching を試してみた

この記事は 弁護士ドットコム Advent Calendar 2020 22日目の記事です。
昨日の記事は @komtaki さんの [オオカミ少年にならないフロントエンドエラー監視システムの工夫 - Qiita](https://qiita.com/komtaki/items/ca35d3e0aee73708a540) でした。システム監視。大事ですね。

本日は OSS ビルドツール **Bazel** のリモートキャッシュ機能を試してみた記事です。

# Bazel とは？
Bazel は Google が開発した Make, Maven, Gradle と似た OSS のビルド及びテストツールです。主な特徴としては下記が挙げられています。

- **High-level build language** : 抽象的でヒトが読める記述を採用していて、従来の複雑な記述からユーザーを保護します。（その分内部で何をやっているかぱっと見てわからないところもあるかもしれませんが、後述する拡張性の恩恵を受けていると思います）
- **Bazel is fast and reliable** : すべての作業をキャッシュし、必要なものだけを再構築します。高速化のためにローカルキャッシュや分散キャッシュ、依存関係分析や並列実行を行います。
- **Bazel is multi-platform** : Linux, macOS, Windows で動作しますし、同じプロジェクトからデスクトップ、サーバー、モバイルなど複数のパッケージを構築できます。
- **Bazel scales** : 10万個以上のソースファイル、数万のリポジトリに対してもアジリティを維持します。（これはGoogle で実際に活用されているという点も魅力の一つに感じます）
- **Bazel is extensible** : 既に多くのプログラム言語がサポートされていますが、Bazel を拡張して他の言語やフレームワークをサポートすることができます。

[公式ページ](https://docs.bazel.build/versions/3.7.0/bazel-overview.html#why-should-i-use-bazel)

# Remote caching とは？
色々な特徴を挙げていますが、今回は中でも分散キャッシュ（Remote caching）の機能に注目していきます。
Remote caching とは開発チームや CI で出力したビルド結果を共有する機能です。
結果を共有することで別のユーザーと結果を再利用でき、ビルドの高速化に繋がります。

# まずは
今回は Remote caching 機能に焦点を当ててますが、Bazel 特有の用語やビルド成果物の生成までを簡単に説明します。言語や環境は以下となります。

- Bazel 3.7.0-homebrew
- Gazelle （後述します）
- golang 1.15

### Bazel 独自の用語
Bazel を活用する上で独自の用語がいくつか出てきます。
今回必要となる用語を簡単に紹介します。

- Workspace
 -  ビルド全般の設定や外部依存解決の定義を記述するファイル
- BUILD.bazel
 - ビルドに必要なファイルやテスト・ビルド・コンテナのプッシュ方法などを記述するファイル（ソースコードが配置されたディレクトリごとに配置する）
- Rules
 - `Workspace` や `BUILD.bazel` に記述する命令の書式 （言語や機能単位で分割されていて [go-rules](https://github.com/bazelbuild/rules_go)のようになっている）

### Gazelle
ディレクトリ単位で生成される `BUILD.bazel` を全て手作業で管理するのは大変です。
そこでこれを自動生成してくれるのが `Gazelle` です。 `Gazelle` は Golang と Protocol buffers をネイティブサポートしています。今回はこちらも利用していきます。

[Gazelle](https://github.com/bazelbuild/bazel-gazelle)

# Bazel Hello World
それでは実際に Bazel を使っていきます。
サンプルは [こちら](https://github.com/oden7777/go-bazel) に上げています。
下記のような簡単なディレクトリ構成となっています。 

```:ディレクトリ構成
.
├── BUILD.bazel            # bazel-gazelle の set-up 箇所を記述
├── README.md
├── WORKSPACE              # bazel-gazelle の set-up 箇所を記述
├── deps.bzl               # 自動生成 （bazel run の時に依存解決部分を分割）
├── go.mod
├── go.sum
└── service
    ├── BUILD.bazel    # 自動生成
    ├── main.go
    └── main_test.go
```

`WORKSPACE` と `BUILD.bazel` は [公式](https://github.com/bazelbuild/bazel-gazelle) の記述をそのままコピーしていきます。


```:WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "io_bazel_rules_go",
    sha256 = "6f111c57fd50baf5b8ee9d63024874dd2a014b069426156c55adbf6d3d22cb7b",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.25.0/rules_go-v0.25.0.tar.gz",
        "https://github.com/bazelbuild/rules_go/releases/download/v0.25.0/rules_go-v0.25.0.tar.gz",
    ],
)

http_archive(
    name = "bazel_gazelle",
    sha256 = "b85f48fa105c4403326e9525ad2b2cc437babaa6e15a3fc0b1dbab0ab064bc7c",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.2/bazel-gazelle-v0.22.2.tar.gz",
        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.2/bazel-gazelle-v0.22.2.tar.gz",
    ],
)

load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")

go_rules_dependencies()

go_register_toolchains(version = "1.15.5")

gazelle_dependencies()
```

```:BUILD.bazel
load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/example/project
gazelle(name = "gazelle")
```

後は Bazel コマンドで自動生成していきます。
下記コマンドでディレクトリ階層ごとの `BUILD.bazel` が生成されビルド・テストの方法が記述されます。

```sh:initialize
$ bazel run //:gazelle 
```

また、下記コマンドで依存関係の管理を行えます。
`-to_macro` オプションを入れることで 記述先を `WORKSPACE` から変更（deps.bzl） でき煩雑さが和らぎます。

```sh:dependency-management
$ bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=deps.bzl%go_dependencies
```

ここまで実施すると テスト・起動・ビルド ができるようになります。
コマンドはそれぞれ以下となります。

```sh:bazel-command
# テスト実行
$ bazel test //...

# アプリ実行
$ bazel run //service

# ビルド & 実行
$ bazel build //service 
$ ./bazel-bin/service/service_/service
```

# Remote caching を試してみた
だいぶ前置きが長くなってしまいました :bow: 
ようやくここからは Remote caching を試していきます。

## キャッシュサーバーの設定
キャッシュサーバーには自前の nginx サーバーやクラウドサービスなどいくつか選択肢があります。今回は [Google Cloud Storage](https://cloud.google.com/storage?hl=ja) を利用します。

GCS を利用したリモートキャッシュの設定の流れは以下となります。

1. GCS で新しいバケットを作成 （バケット名は後で使用します）
2. バケットを利用するサービスアカウントを作成
3. サービスアカウントのIAM権限を追加 （今回は `Storage オブジェクト管理者` を追加しました）
4. 作成したサービスアカウントの秘密鍵 （JSON） を作成して保存 （こちらも後で使用します）

ここまでで下準備が整いました。それでは実行していきましょう。 

## リモートキャッシュを使用したビルド

ビルド時に `--remote_cache` , `--google_credentials` を追加します。
**バケット名**と**JSON鍵**は上記で作成したものに適宜変更します。

```sh:remote-caching_1st
$ bazel build //service \
    --remote_cache=https://storage.googleapis.com/バケット名 \
    --google_credentials=/path/to/JSON鍵.json

-------------  中略  ---------------
INFO: Elapsed time: 0.348s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
```

しかし結果を見ると `1 internal.` となっており、GCS上にも何もアップロードされていません。
これは一度ビルドしているのでローカルキャッシュが適応されているためです。
ローカルキャッシュを削除してみましょう。

```sh:bazel-clean
$ bazel clean
```

もう一度実行します。

```sh:remote-caching_2nd
$ bazel build //service \
    --remote_cache=https://storage.googleapis.com/バケット名 \
    --google_credentials=/path/to/JSON鍵.json

-------------  中略  ---------------
INFO: Elapsed time: 18.232s, Critical Path: 6.85s
INFO: 8 processes: 4 internal, 4 darwin-sandbox.
INFO: Build completed successfully, 8 total actions
```

今度はローカルキャッシュを使わずバイナリを生成してます。GCS上にもキャッシュが上がりました。

```sh:gcs-backet
$ gsutil ls gs://バケット名
gs://バケット名/ac/
gs://バケット名/cas/
```

再度、ローカルキャッシュを削除してみましょう。今度はバイナリをそのまま生成せずにリモートキャッシュを使用するはずです。

```sh:remote-caching_3rd
$ bazel clean

$ bazel build //service \
    --remote_cache=https://storage.googleapis.com/バケット名 \
    --google_credentials=/path/to/JSON鍵.json

-------------  中略  ---------------
INFO: Elapsed time: 13.750s, Critical Path: 2.25s
INFO: 8 processes: 4 remote cache hit, 4 internal.
INFO: Build completed successfully, 8 total actions
```
`4 remote cache hit` となってリモートキャッシュを使用した事がわかります。

おわりの前に簡単な Tips も紹介します。


# Tips
公式にも記載がありますが、試した際に知見もざっくり紹介します。

## .bazelrc
現状のままだとコマンドが長く見通しが悪いです。
Bazel ではサブコマンドのオプションのデフォルト値を設定できます。
`~/.bazelrc` ファイルを作成して以下のように設定することで、実行時にオプション値の設定が不要になります。

```sh:.bazelrc
build --remote_cache=https://storage.googleapis.com/バケット名 --google_credentials=/path/to/JSON鍵.json
test  --remote_cache=https://storage.googleapis.com/バケット名 --google_credentials=/path/to/JSON鍵.json
run   --remote_cache=https://storage.googleapis.com/バケット名 --google_credentials=/path/to/JSON鍵.json
```

## 読み取り専用
リモートキャッシュへの書き込み・読み込みを制限したいといったケースはあると思います。
CI からのみ書き込みを許可して、他は読み取り専用にするといったケースです。
そういった場合は `--remote_upload_local_results=false` を追加します。

```sh:bazel-command
bazel build //service --remote_upload_local_results=false
```

# おわりに
Bazel の簡単な利用方法と Remote caching 機能を試してみました。
個人的には独自の概念があったりで取っ付き難かったですが、徐々にルールなどがわかってきたら触っていて面白かったです。 共通処理の関数化だったり、ルールの拡張もできるとのことだったのでこの辺もやってみたいと思いました。

また [サンプル](https://github.com/oden7777/go-bazel) には Docker イメージを生成するルールの記述もしています。ざっくりコマンドを載せてますので是非試してみてください。
その他にも k8s のルールもあるのでこの辺は自分でもチャレンジしてみようと思います。

明日は @ug23 さんです！ お楽しみに！！ :hand_splayed: