4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

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

この記事はOpenSaaS Studio Advent Calendar 2019、3日目の記事です。

はじめに

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

前提

  • マイクロサービスはGolang+Pythonで構成
    • この記事ではGolangの方の話はありません。
    • 各サービスはDockerイメージにビルドされ、Kubernetes上で実行されている。
    • 試してみる前のビルドプロセス
      • Golangはソースコードのビルド、Dockerイメージの作成、作成したイメージのpushまでBazelで実行
      • Pythonはdocker buildでDockerイメージを作成し、そのイメージをpush
  • Bazelとモノレポ
    • Bazel
      • 複数言語に対応しているビルドツール。複数の言語のビルドルールを同じ記法で定義できる。
    • モノレポ
      • 複数のサービスを一つのリポジトリで管理する。リファクタリングやコードレビューが楽だったり、ライブラリ同士の依存関係が複雑にならなくて済む。
    • mixiさんのこちらの記事が分かりやすかったです。
  • 筆者のPython経験はほぼなし
    • ネットを調べつつslack通知用のアプリをAWS Lambdaで作ったくらい。

モチベーション

  • protoファイルのバージョンを管理を楽にしたい。
    • protoファイルを変更すると、リポジトリが分かれている場合はそれぞれに変更を反映する必要がある。
    • リポジトリが一つだけならその中のprotoファイルを変更すればOK。
  • ビルド定義/プロセスの見通しをよくしたい。
    • 前提で書いているように言語によってビルドツールやプロセスが違っていた。
    • 一つのビルドツール/プロセスに集約することでビルドに関するコンテキストの切り替えを減らせる、といいな。
  • CI/CDのジョブを減らして管理するものを減らしたい。
    • Bazelで透過的にビルドができれば、言語ごとの作り込みもそこまで必要なくジョブをまとめられる、かもしれない。

詰まったポイント

Python2系と3系の両方が必要になる

サービスで使っているのはPython3系だけでしたが、作成したイメージをpushする際にPython2系が必要になりました。
コンテナのイメージレジストリとしてContainer Registryを使っていたので、Bazelがイメージをpushする際にGoogle Cloud SDKが必要となりそれに紐づく形でPython2系が必要になったのではないかなと。
Google Cloud SDKの記述

最近のバージョンの 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ルールを使うと、ライブラリをダウンロードする際にpythonコマンドに紐づいたpipが実行されるようです。上で書いたようにデフォルトのpythonが2系だったためPython3系のライブラリがダウンロードできまぜんでした。
コードだとこのあたり

どうしたか

forkして3系が使えるようにpython->python3.6変更しました。
ただし、最新のバージョンを確認したところデフォルトでpip3に対応してそうだったので今は特に問題ないかもしれません。

作成したイメージのエントリーポイントで苦しむ

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コマンドが設定されておらず、起動できない状態でした。
コードだとこのあたり

どうしたか

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と同じ事象が発生しました。
ライブラリをpip installでインストールした時とBazelでビルドした時でディレクトリ構成が異なることが原因で問題が起きていたようです。

どうしたか

issueにもコメントがありますが、legacy_create_initという属性を指定すると実行できるようになりました。

Dockerイメージとビルド環境のプラットフォームが違う

一部のサービスでtensorflow-gpuを使用しており、ビルド環境にはGPUがなかったためライブラリがダウンロードできませんでした。

どうしたか

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

まとめ

  • GPUなど特殊なプラットフォームが必要でなければPythonのビルドにBazelを使うことはできそう。
    • GPUが必要な場合にローカル環境とCI環境でビルドやテストが両立できるかは不明。
  • ただし、Bazel自体の学習コストに加えてPython用のルールに癖があるため、Pythonのみの環境では採用するメリットが薄そう。
  • 複数の言語で構成されている場合には、ビルド定義やプロセスが統合できるため採用するメリットはあるかもしれない。
    • Bazelはビルドが速いので、モノレポでビルドに時間がかかる場合にはBazelを採用する利点があるかも。
  • Google Cloud SDKのPython2系への依存がなくなるともう少し楽になりそう。EOLまで一ヶ月切ってるけど、それまでにPython3系だけでよくなるんだろうか?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?