BazelでビルドするマルチプロジェクトとCircleCIを連携する
今回のエントリではBazelでビルドしたマルチプロジェクトとCircleCIの連携をまとめていく。マルチプロジェクトとCIを連携する際の実運用の課題を洗い出して解決方法を考えた。
プロジェクト構成
CIと連携させるマルチプロジェクトは次のような構成である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30  | 
(bazel-multiprojects) $ tree -I 'bazel-*|common_kt*|public_kt*|script'
.
├── BUILD
├── Makefile
├── README.md
├── WORKSPACE
├── pkg
│   ├── common_go
│   │   └── util
│   │       ├── BUILD.bazel
│   │       ├── string.go
│   │       └── string_test.go
│   └── public_go
│       ├── BUILD.bazel
│       ├── injector.go
│       ├── main.go
│       ├── main_test.go
│       ├── usecase
│       │   ├── BUILD.bazel
│       │   └── greet_usecase.go
│       └── wire_gen.go
└── proto
    ├── echo
    │   ├── BUILD.bazel
    │   ├── echo.pb.go
    │   └── echo.proto
    └── greet
        ├── BUILD.bazel
        ├── greet.pb.go
        └── greet.proto | 
CIと連携する上で実現したいこと
pkg以下にはcommon_goとpublic_goのパッケージが配置されている。
public_goはcommon_goに依存している。
CIと連携する上で実現したいことを一覧にする。
- パッケージ内のコードに修正が入ればパッケージのテストを実行したい
 - パッケージ内のコードに修正が入ればパッケージのバイナリビルドを実行したい
 - パッケージ内のコードに修正が入りコンテナビルドのタスクがあればレジストリへプッシュを行いたい
 - common_goに修正が入れば依存している全てのパッケージのテストからバイナリビルド、コンテナビルドとレジストリへのプッシュを行いたい
 
ここから1つずつ実現方法をまとめていく。
CircleCIのDockerイメージにbazelをインストールする
CircleCIのDockerイメージにはbazelは標準でセットアップされていないのでインストールする必要がある。
Bazelをインストールするジョブは次のようになった。
 | 
 | 
Installing Bazel on Ubuntu - Bazel
公式ドキュメントに載っているとおりのインストール手順をCI上で実行する。
パッケージ内のコードに修正が入ればパッケージのテストとバイナリビルドを実行したい
テストとバイナリビルドの方法をまとめていく。
方法としてはCI上にチェックアウトされたコードとmasterまたはHEADとの差分をとり修正されたコードのパスを取得する。そのパスからbazelコマンドに必要な文字列を抜き出せば良い。
先人の知恵を借りてBazel連携を実現した。
CircleCIで変更があった箇所だけに限定してビルドするテクニック · tehepero note(・ω<) 2.0
完成したシェルスクリプトは次のようになった。
 | 
 | 
common_goとpublic_goに修正が入ればcheck.tmpには次のような文字列が格納される。
1 2  | 
common_go public_go  | 
check.tmpの1行ずつを抜き出してbazel queryを使いテストとバイナリビルドに合致するビルドタスクを抽出して実行している。
bazel build //pkg/…のようにして全体をビルドせずに更新のあったパッケージのみビルドすることができた。
local_resourcesのフラッグを指定する
protobufが含まれるプロジェクトのためビルドに必要なメモリを確保する必要がある。Dockerイメージ内でBazelを動かしているのでlocal_resourcesのフラッグを指定してメモリ調整をしている。
1
 | 
--local_resources=4096,2.0,1.0  | 
これを指定しないとOOMが発生してしまうので注意が必要である。
パッケージ内のコードに修正が入りコンテナビルドのタスクがあればレジストリへプッシュを行いたい
Dockerイメージをビルドするタスクを container_pushのnameに統一してbazel queryで抽出することで更新のあったパッケージのみがレジストリにプッシュされるようにした。
1 2 3  | 
for pkgname in `cat check.tmp`; do
  ${PROJECT_DIR}/bin/bazel query //... | awk "/^\/\/pkg\/$pkgname:container_push$/"  | xargs ${PROJECT_DIR}/bin/bazel run --define IMAGE_TAG=${IMAGE_TAG}
done | 
またcontainer_pushのタスクは次のように定義してある。
 | 
 | 
tagに”$(IMAGE_TAG)”を指定することでビルド時のオプションを参照できるようにした。ビルドのオプションに–define IMAGE_TAG=${IMAGE_TAG}を加えることでブランチごとのDockerイメージがプッシュできる。
1 2 3 4  | 
CURRENT_BRANCH=`git rev-parse --abbrev-ref @`
IMAGE_TAG=${CURRENT_BRANCH/\//_}
${PROJECT_DIR}/bin/bazel query //... | awk "/^\/\/pkg\/$pkgname:container_push$/"  | xargs ${PROJECT_DIR}/bin/bazel run --define IMAGE_TAG=${IMAGE_TAG} | 
rule内のIMAGE_TAGを変数化する方法は他にもありworkspace_status_command を使えば変数をファイルで管理できる。
GitHub - bazelbuild/rules_docker: Rules for building and handling Docker images with Bazel
common_goに修正が入れば依存している全てのパッケージのテストからバイナリビルド、コンテナビルドとレジストリへのプッシュを行いたい
最後に共通パッケージに修正が入れば依存するパッケージすべてにビルドタスクを実行するための実現方法をまとめる。
方法は2つある。
1つ目はrdepを使って依存するパッケージをqueryで抽出する方法である。
1 2 3  | 
bazel query 'rdeps(pkg/..., pkg/common_go/...)' --output package pkg/common_go/util pkg/public_go  | 
pkg以下でpkg/common_goが利用されているパッケージが抽出できる。
2つ目はシンプルにbazel build //pkg/…を実行してしまう方法である。
共通パッケージに依存が多ければビルド時間が長くなっていくのでCIで自動化するべきかはプロジェクト規模による。自動化せずにChatOpsなどで特定のパッケージをビルドするインターフェースを用意したほうが効率が良いかもしれない。
まとめ
- BazelでビルドするマルチプロジェクトとCircleCIを連携する方法をまとめた。
 - CIのDockerイメージ内のメモリ調整にlocal_resourcesのフラッグを指定する知見が溜まった。
 - bazel queryを駆使して実行したいタスクを抽出すればCI連携も見通しよく整理できそうな感触を得た。