Bazelで(Golang+)Pythonのモノレポにトライしてみた話

この記事は[OpenSaaS Studio Advent Calendar 2019](https://adventar.org/calendars/3928)、3日目の記事です。

# はじめに
GolangとPythonのマイクロサービスで前者がBazelを使ったモノレポ構成。複数言語に対応しているBazelを使えばPythonもモノレポに混ぜられるかな、と試してみたら大変だったので苦労したポイントを残します。

# 前提
* マイクロサービスはGolang+Pythonで構成
    * この記事ではGolangの方の話はありません。
    * 各サービスはDockerイメージにビルドされ、Kubernetes上で実行されている。
    * 試してみる前のビルドプロセス
        * Golangはソースコードのビルド、Dockerイメージの作成、作成したイメージのpushまでBazelで実行
        * Pythonは`docker build`でDockerイメージを作成し、そのイメージをpush
* Bazelとモノレポ
    * [Bazel](https://bazel.build/)
        * 複数言語に対応しているビルドツール。複数の言語のビルドルールを同じ記法で定義できる。
    * モノレポ
        * 複数のサービスを一つのリポジトリで管理する。リファクタリングやコードレビューが楽だったり、ライブラリ同士の依存関係が複雑にならなくて済む。
    * mixiさんの[こちらの記事](https://medium.com/mixi-developers/bazel%E3%81%A8%E3%83%A2%E3%83%8E%E3%83%AC%E3%83%9D-b901ffba61ce)が分かりやすかったです。
* 筆者のPython経験はほぼなし
    * ネットを調べつつslack通知用のアプリをAWS Lambdaで作ったくらい。
 
# モチベーション
* protoファイルのバージョンを管理を楽にしたい。
    * protoファイルを変更すると、リポジトリが分かれている場合はそれぞれに変更を反映する必要がある。
    * リポジトリが一つだけならその中のprotoファイルを変更すればOK。
* ビルド定義/プロセスの見通しをよくしたい。
    * 前提で書いているように言語によってビルドツールやプロセスが違っていた。
    * 一つのビルドツール/プロセスに集約することでビルドに関するコンテキストの切り替えを減らせる、といいな。
* CI/CDのジョブを減らして管理するものを減らしたい。
    * Bazelで透過的にビルドができれば、言語ごとの作り込みもそこまで必要なくジョブをまとめられる、かもしれない。

# 詰まったポイント
## Python2系と3系の両方が必要になる
サービスで使っているのはPython3系だけでしたが、作成したイメージをpushする際にPython2系が必要になりました。
コンテナのイメージレジストリとして[Container Registry](https://cloud.google.com/container-registry/docs/overview)を使っていたので、Bazelがイメージをpushする際にGoogle Cloud SDKが必要となりそれに紐づく形でPython2系が必要になったのではないかなと。
[Google Cloud SDKの記述](https://cloud.google.com/sdk/docs/?hl=ja)
> 最近のバージョンの macOS には、Google Cloud SDK に必要な Python の適切なバージョンが含まれています。Cloud SDK には、リリース番号が Python 2.7.9 以降の Python 2 が必要です。追加の Python インタープリタをインストールする場合、それが Google Cloud SDK のインストールを妨げるものであってはなりません。

### どうしたか
Bazelにはビルドする際に使用するPythonのインタープリタを指定できる仕組みがあるので、それを利用しました。

* pyenvでPython2.7とPython3.6をインストール。
  * Python2.7をデフォルトにすることで、Bazelでイメージをpushする時にPython2系が使われるようにする。
* ビルド時に使用するPython3系のインタープリタを定義。
* Python3系でビルドしたい対象にバージョンを指定。

インタープリタの定義とビルド定義はこんな感じ。

* BUILD

```
load("@rules_python//python:defs.bzl", "py_runtime_pair")

py_runtime(
    name = "py2_local_bin_runtime",
    interpreter_path = "/usr/local/bin/python2",
    python_version = "PY2",
)

py_runtime(
    name = "py3_6_local_bin_runtime",
    interpreter_path = "/usr/local/bin/python3.6",
    python_version = "PY3",
)

py_runtime_pair(
    name = "py_local_bin_runtime_pair",
    py2_runtime = ":py2_local_bin_runtime",
    py3_runtime = ":py3_6_local_bin_runtime",
)

toolchain(
    name = "py_mac_toolchain",
    exec_compatible_with = [
        "@bazel_tools//platforms:osx",
        "@bazel_tools//platforms:x86_64",
    ],
    toolchain = "py_local_bin_runtime_pair",
    toolchain_type = "@rules_python//python:toolchain_type",
)
```

* WORKSPACE
 
```
register_toolchains(
    "//:py_mac_toolchain",
    "//:py_linux_toolchain",
)
```

* BUILD.bazel

```
py_binary(
    name = "mysite",
    srcs = ["manage.py"],
    deps = [
        "//mysite/mysite:py_default_library",
        "//mysite/polls:py_default_library",
    ],
    main = "manage.py",
    python_version = "PY3",
)
```


## Python3に対応したライブラリが取得できない
[Python用のBazelルール](https://github.com/bazelbuild/rules_python)を使うと、ライブラリをダウンロードする際に`python`コマンドに紐づいた`pip`が実行されるようです。上で書いたようにデフォルトの`python`が2系だったためPython3系のライブラリがダウンロードできまぜんでした。
コードだと[このあたり](https://github.com/bazelbuild/rules_python/blob/9d68f24659e8ce8b736590ba1e4418af06ec2552/python/pip.bzl#L27)

### どうしたか
forkして3系が使えるように`python`->`python3.6`に[変更](https://github.com/mmmknt/rules_python/blob/master/python/pip.bzl#L27)しました。
ただし、最新のバージョンを確認したところデフォルトで[pip3に対応してそう](https://github.com/bazelbuild/rules_python/pull/256)だったので今は特に問題ないかもしれません。

## 作成したイメージのエントリーポイントで苦しむ
BazelにはPython3系のDockerイメージを作成するためのデフォルトのルール(`py3_image`)があり、python:3.6-slimと組み合わせてビルドしたところ、作成したイメージが起動できませんでした。
ビルド定義はこんな感じ

* WORKSPACE

```
container_pull(
    name = "py3_base",
    registry = "index.docker.io",
    repository = "library/python",
    tag = "3.6-slim",
)
```

* BUILD.bazel

```
py3_image(
    name = "mysite_image",
    srcs = ["manage.py"],
    deps = [
        "//mysite/mysite:py_default_library",
        "//mysite/polls:py_default_library",
    ],
    main = "manage.py",
    base = "@py3_base//image",
)
```

実は`py3_image`で作成されたイメージはデフォルトでエントリーポイントが`/usr/bin/python`になりますが、python:3.6-slimではそのパスにpythonコマンドが設定されておらず、起動できない状態でした。
コードだと[このあたり](https://github.com/bazelbuild/rules_docker/blob/v0.9.0/python3/image.bzl#L104)

### どうしたか
`python:3.6`だと正常に起動できたためこのイメージを使用しました。（イメージの最適化は必要に応じて後で実施する想定）

## Pythonのバージョンがサービスごとに違う
Pythonのサービスが3つあり、使用しているPythonのバージョンが3.5、3.6、3.7と全て違っていました。
上で記載しているPythonのインタープリタを使い分ける方法では2系と3系しか分けられないためこの仕組みでは対応ができません。

### どうしたか
サービスで使用するPythonのバージョンを全て3.6に寄せることにしました。
ここまでの検証で時間がかかっていたこと、ビルド周りの構成を複雑にしていくのも当初の方針から外れるかなと思ったので、管理するものを減らす方針を取りました。
もしPython3系の複数のバージョンを共存したい場合は、WORKSPACEを分けてそれぞれで必要なPythonのインタープリタを指定すればできるかもしれません。（未確認）

* 構成のイメージ

```
├── service_for_python3.6
│   └── WORKSPACE <-ここにPython3.6のインタープリタを設定
├── service_for_python3.7
│   └── WORKSPACE <-ここにPython3.7のインタープリタを設定
└── WORKSPACE
```

## `__init__.py`のimportが壊れる
（おそらく）google-cloud-XXX系のライブラリで[このissue](https://github.com/bazelbuild/rules_python/issues/55)と同じ事象が発生しました。
ライブラリを`pip install`でインストールした時とBazelでビルドした時でディレクトリ構成が異なることが原因で問題が起きていたようです。

### どうしたか
issueにも[コメント](https://github.com/bazelbuild/rules_python/issues/55#issuecomment-403756388)がありますが、`legacy_create_init`という属性を指定すると実行できるようになりました。

## Dockerイメージとビルド環境のプラットフォームが違う
一部のサービスでtensorflow-gpuを使用しており、ビルド環境にはGPUがなかったためライブラリがダウンロードできませんでした。

### どうしたか
未解決です。
検証を始めてからそれなりに時間が経過していたため、ここで切り上げました。
（存在するなら）GPUに紐づくplatformを指定するか、GPUがのっているビルドファームでこのサービスをビルドするようにすれば、Bazelで対応できるかもしれません。

# まとめ
* GPUなど特殊なプラットフォームが必要でなければPythonのビルドにBazelを使うことはできそう。
    * GPUが必要な場合にローカル環境とCI環境でビルドやテストが両立できるかは不明。
* ただし、Bazel自体の学習コストに加えてPython用のルールに癖があるため、Pythonのみの環境では採用するメリットが薄そう。
* 複数の言語で構成されている場合には、ビルド定義やプロセスが統合できるため採用するメリットはあるかもしれない。
    * Bazelは[ビルドが速い](https://redfin.engineering/we-switched-from-maven-to-bazel-and-builds-got-10x-faster-b265a7845854)ので、モノレポでビルドに時間がかかる場合にはBazelを採用する利点があるかも。
* Google Cloud SDKのPython2系への依存がなくなるともう少し楽になりそう。[EOLまで一ヶ月切ってる](https://python3statement.org/)けど、それまでにPython3系だけでよくなるんだろうか？