マルチコンテナなサービス構成でgRPC in Pythonのpb2を共有する話

チーム開発時、複数のDockerコンテナ間をgRPCで通信するケースが出てくる。

各コンテナをPythonで実装する場合、*pb2*.pyの共有にはどんな方法があるか?

背景

複数のDockerコンテナでgRPC通信する環境の場合、以下のようなファイル構成が想定される(Docker Composeを使う場合)。

backend/
- docker-compose.yml
- service_main/: mainサービスのコード・コンテナ構成
- service_sub1/: sub1サービスのコード・コンテナ構成
- service_sub2/: sub2サービスのコード・コンテナ構成
- proto/
  - interface_main_sub1.proto
  - interface_main_sub2.proto

この場合、proto/*.protoをprotocでコンパイルすると*pb2*.pyが生成される(--python_out, --grpc_python_outの指定が必要)。

*pb2*.pyを各サービスで共有できると、IDEでコード補完が効くほか、各コンテナを並行で開発できる。

しかし、pythonの制約上パッケージ外の相対importはできない※。

※ python2だとできるが、サポート終了済みなので除外

取りうる方法

以下の方法が考えられる。

  1. 生成された*pb2*.pyを直接各サービスのディレクトリにコピーしておく
  2. あらかじめ*pb2*.pyをライブラリにしておく
  3. sys.pathにルートディレクトリの絶対パスを追加する

3.について、sys.pathへの追加は実行時に行われる。したがって、結局コード補完が効かないので除外。

本ページでは、上記1.および2.について記載する。

手法1. 生成された*pb2*.pyを直接各サービスのディレクトリにコピーしておく

通常はこの方法で問題なく、簡便に運用・管理できる。protoを更新してinterfaceが変わった場合、IDE上でエラーが発生するので検知もできる。ただし、コードをIDEで開かないとエラーを検知できないので、CI/CDで静的解析するステップを入れて検知できるのが望ましい。

やり方

上記ファイル構成を基に記載する。

  1. proto/ にコンパイル・pb2共有用のshell scriptを追加する。本scriptでは、protoをコンパイル後*pb2*.pyを各サービスに展開する。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

proto_sub1="sub1"
proto_sub2="sub2"

# compile *.proto
python3 -m grpc_tools.protoc \
    ${proto_sub1}.proto --proto_path=. \
    --python_out=. --grpc_python_out=.
python3 -m grpc_tools.protoc \
    ${proto_sub2}.proto --proto_path=. \
    --python_out=. --grpc_python_out=.

# move pb2 to each service
cp ${proto_sub1}*pb2*.py ../service_main
cp ${proto_sub1}*pb2*.py ../service_sub1

cp ${proto_sub2}*pb2*.py ../service_main
cp ${proto_sub2}*pb2*.py ../service_sub2

rm *pb2*.py
  1. protoファイルを更新するたび、上記shell scriptを実行。更新した*.protoおよび*pb2*.pyをVCSにcommitする。

コード例

https://github.com/Niccari/py_rest_grpc_benchmark/tree/main  

手法2. あらかじめ*pb2.pyをライブラリにしておく

どうしてもprotoを明示的にバージョニングする必要がある場合、この手法を使うことができる。

ただし、ライブラリ生成および各サービスでライブラリ更新が必要になり、やや煩雑。

やり方

  1. proto/のフォルダ構成を以下に変更する。
(sub1向けライブラリ名)/
  - __init__.py
  - setup.py
(sub2向けライブラリ名)/
  - __init__.py
  - setup.py
- interface_main_sub1.proto
- interface_main_sub2.proto

__init__.pyについては空でよい。setup.pyは以下のようにsetup関数を実行するようにする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from setuptools import setup

setup(
    name='ライブラリ名',
    version='protoに対するバージョン(0.0.1など)',
    description='proto定義についての簡潔な説明',
    author='ライブラリ制作元',
    install_requires=["grpcio", "protobuf"],    # *pb2*.py内でimportされるので必要
    packages=["*向けライブラリ名"],
)
  1. proto/ にコンパイル・pb2共有用のshell scriptを追加。本scriptでは、protoをコンパイル後*pb2*.pyをライブラリにする。ライブラリを各サービスに展開する。
 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
31
32
33
proto_sub1="sub1"
proto_sub2="sub2"
lib_sub1_path="./(sub1向けライブラリ名)"
lib_sub2_path="./(sub2向けライブラリ名)"

# compile *.proto
python3 -m grpc_tools.protoc \
    ${proto_sub1}.proto --proto_path=. \
    --python_out=${lib_sub1_path} --grpc_python_out=${lib_sub1_path}
    python3 -m grpc_tools.protoc \
    ${proto_sub2}.proto --proto_path=. \
    --python_out=${lib_sub2_path} --grpc_python_out=${lib_sub2_path}

# Make pb2 importable
sed -i '' -e "s/^import ${proto_sub1}_pb2/from . import ${proto_sub1}_pb2/" "${lib_sub1_path}/${proto_sub1}_pb2_grpc.py"
sed -i '' -e "s/^import ${proto_sub2}_pb2/from . import ${proto_sub2}_pb2/" "${lib_sub2_path}/${proto_sub2}_pb2_grpc.py"

# build libraries, then move them to each service
cd ${lib_sub1_path}
python3 setup.py bdist_wheel
rm -r build *.egg-info
cp dist/*.whl ../../service_main
cp dist/*.whl ../../service_sub1
rm -r dist
cd ..

cd ${lib_sub2_path}
python3 setup.py bdist_wheel
rm -r build *.egg-info
cp dist/*.whl ../../service_main
cp dist/*.whl ../../service_sub2
rm -r dist
cd ..
  1. 各サービスごとに、生成されたライブラリをインストールする。更新した*.proto, requirements.txtおよび生成されたwhlをVCSにcommitする。

コード例

https://github.com/Niccari/py_rest_grpc_benchmark/tree/proto_as_lib

以上