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_gopublic_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連携を実現した。

blog.stormcat.io

完成したシェルスクリプトは次のようになった。

#!/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連携も見通しよく整理できそうな感触を得た。

コード

github.com