BazelでビルドするマルチプロジェクトとCircleCIを連携する
今回のエントリではBazelでビルドしたマルチプロジェクトとCircleCIの連携をまとめていく。マルチプロジェクトとCIを連携する際の実運用の課題を洗い出して解決方法を考えた。
プロジェクト構成
CIと連携させるマルチプロジェクトは次のような構成である。
(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をインストールするジョブは次のようになった。
# .circleci/config.yml jobs: preparetool: docker: - image: circleci/golang:1.10.3 steps: - restore_cache: keys: - v1-build-bazel-cache-{{ .Branch }}-- - v1-build-bazel-cache-master-- - v1-build-bazel-cache- - run: name: setup command: | if ! type ./bin/bazel >/dev/null 2>&1; then sudo apt-get install pkg-config zip g++ zlib1g-dev unzip python wget -q -O bazel-installer.sh https://github.com/bazelbuild/bazel/releases/download/0.19.2/bazel-0.19.2-installer-linux-x86_64.sh chmod +x ./bazel-installer.sh ./bazel-installer.sh --user mkdir -p ./bin cp /home/circleci/.bazel/bin/* ./bin/ fi - run: name: version command: ./bin/bazel version - save_cache: key: v1-build-bazel-cache-{{ .Branch }}--{{ .Revision }} paths: - ./bin
Installing Bazel on Ubuntu - Bazel
公式ドキュメントに載っているとおりのインストール手順をCI上で実行する。
パッケージ内のコードに修正が入ればパッケージのテストとバイナリビルドを実行したい
テストとバイナリビルドの方法をまとめていく。
方法としてはCI上にチェックアウトされたコードとmasterまたはHEADとの差分をとり修正されたコードのパスを取得する。そのパスからbazelコマンドに必要な文字列を抜き出せば良い。
先人の知恵を借りてBazel連携を実現した。
完成したシェルスクリプトは次のようになった。
#!/usr/bin/env bash PROJECT_DIR=$1 COMMAND=$2 if [[ ${COMMAND} != "build" && ${COMMAND} != "push" ]]; then echo "$COMMAND is invalid command. (Required build or push)." 1>&2 exit 1 fi CURRENT_BRANCH=`git rev-parse --abbrev-ref @` IMAGE_TAG=${CURRENT_BRANCH/\//_} # 変更があったdockerイメージを取得 if [ ${CURRENT_BRANCH} = "master" ]; then # 現在がmasterであれば、直前のコミットと比較 TARGET="HEAD^ HEAD" else # masterブランチ以外であれば、origin/masterの最新と比較 TARGET="origin/master" fi git diff ${TARGET} --name-only | awk '/^pkg/' | awk '{sub("pkg/", "", $0); print $0}' | awk '{print substr($0, 0, index($0, "/") -1)}' | awk '!a[$0]++' > check.tmp for pkgname in `cat check.tmp`; do if [[ ${COMMAND} == "build" ]]; then # test ${PROJECT_DIR}/bin/bazel query //... | grep "//pkg/$pkgname" | xargs ${PROJECT_DIR}/bin/bazel test --define IMAGE_TAG=${IMAGE_TAG} --local_resources=4096,2.0,1.0 # build ${PROJECT_DIR}/bin/bazel query //... | awk "/^\/\/pkg\/$pkgname:$pkgname$/" | xargs ${PROJECT_DIR}/bin/bazel build --define IMAGE_TAG=${IMAGE_TAG} --local_resources=4096,2.0,1.0 elif [[ ${COMMAND} == "push" ]]; then # docker login // should login to docker ${PROJECT_DIR}/bin/bazel query //... | awk "/^\/\/pkg\/$pkgname:container_push$/" | xargs ${PROJECT_DIR}/bin/bazel run --define IMAGE_TAG=${IMAGE_TAG} fi done rm check.tmp
common_goとpublic_goに修正が入ればcheck.tmp
には次のような文字列が格納される。
common_go public_go
check.tmpの1行ずつを抜き出してbazel queryを使いテストとバイナリビルドに合致するビルドタスクを抽出して実行している。
bazel build //pkg/...
のようにして全体をビルドせずに更新のあったパッケージのみビルドすることができた。
local_resourcesのフラッグを指定する
protobufが含まれるプロジェクトのためビルドに必要なメモリを確保する必要がある。Dockerイメージ内でBazelを動かしているのでlocal_resourcesのフラッグを指定してメモリ調整をしている。
--local_resources=4096,2.0,1.0
これを指定しないとOOMが発生してしまうので注意が必要である。
パッケージ内のコードに修正が入りコンテナビルドのタスクがあればレジストリへプッシュを行いたい
Dockerイメージをビルドするタスクを container_push
のnameに統一してbazel queryで抽出することで更新のあったパッケージのみがレジストリにプッシュされるようにした。
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
のタスクは次のように定義してある。
container_push( name = "container_push", image = ":public_go_image", format = "Docker", registry = "index.docker.io", repository = "soushin/bazel-multiprojects-go", tag = "$(IMAGE_TAG)", )
tagに"$(IMAGE_TAG)"
を指定することでビルド時のオプションを参照できるようにした。ビルドのオプションに--define IMAGE_TAG=${IMAGE_TAG}
を加えることでブランチごとのDockerイメージがプッシュできる。
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で抽出する方法である。
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連携も見通しよく整理できそうな感触を得た。