gRPCのコード生成をCircleCIで自動化する


はじめに

gRPCではクライアントとサーバーのインターフェースを一致させる事ができますが

クライアントとスタブのコード生成がずれると通信できなくなります。

また各言語向けのprotocol buffer compilerを環境構築するのは面倒です。

そこで諸々の環境が揃ったDockerイメージのnamely/protoc-allとCircleCIを組合せてコード生成を自動化します。


構成

以下の図とブランチ構成でコード生成の自動化を行います。

plantuml.png


  • 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


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)も一緒に生成します。



  • 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


.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ジョブは次の処理を行います。


  1. git configで必須項目を設定する。${GH_TOKEN}はGitHubのPersonal access tokenを設定する

  2. commit対象のbranchをgit worktreeでcheckoutする

  3. protoのファイル名変更や削除も反映するために一旦全部削除する

  4. 生成したファイルをgit commitする。このブランチはCircleCIのジョブを動かしたくないのでコメントに[ci skip]を入れる

  5. 差分があれば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ブランチからビルドされています。


参考リンク