DEV Community

KOGA Mitsuhiro
KOGA Mitsuhiro

Posted on • Originally published at qiita.com

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

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

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ブランチからビルドされています。

参考リンク

Top comments (0)