はじめに
gRPCではクライアントとサーバーのインターフェースを一致させる事ができますが
クライアントとスタブのコード生成がずれると通信できなくなります。
また各言語向けのprotocol buffer compilerを環境構築するのは面倒です。
そこで諸々の環境が揃ったDockerイメージのnamely/protoc-allとCircleCIを組合せてコード生成を自動化します。
構成
以下の図とブランチ構成でコード生成の自動化を行います。
- masterブランチ
- .circleci/config.yml
- docker-compose.yml
- protos/echo.proto
- Go用のgenerated/goブランチ
- C#用のgenerated/csharpブランチ
また実際の試したものは以下のリポジトリに置いています。
https://github.com/shiena/grpc-gen-circleci
https://github.com/shiena/grpc-gen-circleci/tree/generated/go
https://github.com/shiena/grpc-gen-circleci/tree/generated/csharp
各ファイルについて
docker-compose.yml
version: "3"
services:
lint:
image: namely/protoc-all:1.16_0
volumes:
- .:/defs
entrypoint: "sh -c"
command: '"protoc -I/usr/local/include -I. --lint_out=. ./protos/*.proto"'
go:
image: namely/protoc-all:1.16_0
volumes:
- .:/defs
command: "-d ./protos -o ./pb-go --with-docs markdown,readme.md --with-gateway -l go"
csharp:
image: namely/protoc-all:1.16_0
volumes:
- .:/defs
command: "-d ./protos -o ./pb-csharp --with-docs markdown,readme.md -l csharp"
docker-compose.ymlではlintとGoのコード生成とC#のコード生成を例にサービスを定義します。
- lint
- *.protoをワイルドカード展開したいので敢えてentrypointとcommandを設定しています。
- go
- クライアント、スタブと一緒にgrpc-gateway用のコード(
--with-gateway
)とドキュメント(--with-docs
)も一緒に生成します。
- クライアント、スタブと一緒にgrpc-gateway用のコード(
- csharp
- こちらもクライアント、スタブと一緒にドキュメント(
--with-docs
)を生成します。
- こちらもクライアント、スタブと一緒にドキュメント(
またentrypointに設定されているコマンドは以下のようなオプションがあります。
gen-proto generates grpc and protobuf @ Namely
Usage: gen-proto -f my-service.proto -l go
options:
-h, --help Show help
-f FILE The proto source file to generate
-d DIR Scans the given directory for all proto files
-l LANGUAGE The language to generate (go ruby csharp java python objc gogo php node)
-o DIRECTORY The output directory for generated files. Will be automatically created.
-i includes Extra includes
--lint CHECKS Enable linting protoc-lint (CHECKS are optional - see https://github.com/ckaznocha/protoc-gen-lint#optional-checks)
--with-gateway Generate grpc-gateway files (experimental).
--with-docs FORMAT Generate documentation (FORMAT is optional - see https://github.com/pseudomuto/protoc-gen-doc#invoking-the-plugin)
--go-source-relative Make go import paths 'source_relative' - see https://github.com/golang/protobuf#parameters
.circleci/config.yml
version: 2
references:
defaults: &defaults
working_directory: ~/grpc-gen
machine: true
jobs:
lint:
<<: *defaults
steps:
- checkout
- run:
name: Lint
command: docker-compose run --rm lint
- persist_to_workspace:
root: .
paths:
- .
build:
<<: *defaults
steps:
- attach_workspace:
at: .
- run:
name: Clean up
command: rm -rf pb-go pb-csharp
- run:
name: Generate gRPC for go
command: docker-compose run --rm go
- run:
name: Generate gRPC for csharp
command: docker-compose run --rm csharp
- persist_to_workspace:
root: .
paths:
- .
push:
<<: *defaults
steps:
- attach_workspace:
at: .
- run:
name: git config
command: |
git config user.email "shiena.jp@gmail.com"
git config user.name "Generator Bot"
git remote add upstream https://${GH_TOKEN}@github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git
- run:
name: git commit go-src
command: |
if [ `git branch -r --list origin/generated/go | wc -l` -eq 1 ]; then
echo "checkout generated/go"
git fetch origin
git worktree add -b generated/go ../go-src origin/generated/go
git -C ../go-src rm `git -C ../go-src ls-files`
else
echo "create generated/go"
git worktree add --detach ../go-src
git -C ../go-src checkout --orphan generated/go
git -C ../go-src rm --cached -r .
git -C ../go-src clean -d -f
fi
cp -a pb-go/* ../go-src/
git -C ../go-src add -A
git -C ../go-src status
result=0
git -C ../go-src commit -m "AUTO GENERATED [ci skip]" || result=$?
if [ $result -eq 0 ]; then
git -C ../go-src push upstream generated/go 2> /dev/null
fi
- run:
name: git commit csharp-src
command: |
if [ `git branch -r --list origin/generated/csharp | wc -l` -eq 1 ]; then
echo "checkout generated/csharp"
git fetch origin
git worktree add -b generated/csharp ../csharp-src origin/generated/csharp
git -C ../csharp-src rm `git -C ../csharp-src ls-files`
else
echo "create generated/csharp"
git worktree add --detach ../csharp-src
git -C ../csharp-src checkout --orphan generated/csharp
git -C ../csharp-src rm --cached -r .
git -C ../csharp-src clean -d -f
fi
cp -a pb-csharp/* ../csharp-src/
git -C ../csharp-src add -A
git -C ../csharp-src status
result=0
git -C ../csharp-src commit -m "AUTO GENERATED [ci skip]" || result=$?
if [ $result -eq 0 ]; then
git -C ../csharp-src push upstream generated/csharp 2> /dev/null
fi
workflows:
version: 2
build_and_push:
jobs:
- lint
- build:
requires:
- lint
filters:
branches:
only:
- master
- push:
requires:
- lint
- build
filters:
branches:
only:
- master
CircleCIでは以下の3つのジョブを作っています。
- protoの文法をチェックするlintジョブ
- クライアントとスタブとドキュメントを生成するbuildジョブ
- 各言語毎のブランチに生成物をcommit -> pushするpushジョブ
この中で一番大きなpushジョブは次の処理を行います。
- git configで必須項目を設定する。
${GH_TOKEN}
はGitHubのPersonal access tokenを設定する - commit対象のbranchをgit worktreeでcheckoutする
- protoのファイル名変更や削除も反映するために一旦全部削除する
- 生成したファイルをgit commitする。このブランチはCircleCIのジョブを動かしたくないのでコメントに
[ci skip]
を入れる - 差分があればgit pushする。ここでエラーになるとPersonal access tokenが出力されてしまうので標準エラーを/dev/nullに捨てる
以上の設定でそれぞれのブランチに生成されたコードがpushされるのでsubmoduleで取り込むことができます。
gRPCのバージョン
namely/protoc-allはgRPCの1.14.xブランチをcloneしているのでどのコミットでビルドされたものか分かりません。
そこでv1.15.1のリリースタグをcloneしてビルドしたDockerイメージを作りました。
最新版はv1.16.xブランチからビルドされています。