マルチコンテナなサービス構成で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だとできるが、サポート終了済みなので除外
取りうる方法
以下の方法が考えられる。
- 生成された*pb2*.pyを直接各サービスのディレクトリにコピーしておく
- あらかじめ*pb2*.pyをライブラリにしておく
- sys.pathにルートディレクトリの絶対パスを追加する
3.について、sys.pathへの追加は実行時に行われる。したがって、結局コード補完が効かないので除外。
本ページでは、上記1.および2.について記載する。
手法1. 生成された*pb2*.pyを直接各サービスのディレクトリにコピーしておく
通常はこの方法で問題なく、簡便に運用・管理できる。protoを更新してinterfaceが変わった場合、IDE上でエラーが発生するので検知もできる。ただし、コードをIDEで開かないとエラーを検知できないので、CI/CDで静的解析するステップを入れて検知できるのが望ましい。
やり方
上記ファイル構成を基に記載する。
- 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
|
- protoファイルを更新するたび、上記shell scriptを実行。更新した*.protoおよび*pb2*.pyをVCSにcommitする。
コード例
https://github.com/Niccari/py_rest_grpc_benchmark/tree/main
手法2. あらかじめ*pb2.pyをライブラリにしておく
どうしてもprotoを明示的にバージョニングする必要がある場合、この手法を使うことができる。
ただし、ライブラリ生成および各サービスでライブラリ更新が必要になり、やや煩雑。
やり方
- 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=["*向けライブラリ名"],
)
|
- 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 ..
|
- 各サービスごとに、生成されたライブラリをインストールする。更新した*.proto, requirements.txtおよび生成されたwhlをVCSにcommitする。
コード例
https://github.com/Niccari/py_rest_grpc_benchmark/tree/proto_as_lib
以上