proto定義や成果物の管理用レポジトリを構築した話

こんにちは、Fundsチームの @convto です。Kyashでは銀行入金やコンビニ入金などの残高の入出金に関わる部分の開発をしています。

Fundsチームではその業務の性質上多数の外部ベンダとやり取りをしています。
それぞれベンダごとに仕様なども異なるため、その接続部分のいくつかはマイクロサービスとして切り出されています。

Fundsチームの管理しているサービス間の通信でgRPCを導入する際、今後の社内の別サービスなどにも汎用的に使えるようなproto管理レポジトリを構築したのでその紹介をしたいと思います。

proto管理レポジトリで満たしたい要件について

はじめに、他社の事例も参考にしつつ、自分たちがprotoを管理するにあたってどのような要件を満たせれば良いのか整理しました。

他社の事例を調査したところ、以下のような構成が多かったように思います。

  • 名称は app-protoplatform-proto などの全体で共有することがわかる汎用的な名称が多かった
  • pushするとCIで成果物が生成される
  • サービス単位でディレクトリを切り分けている
  • webやmobileなどのクライアントから叩かれる場合はProtobuf over HTTPなど、別途追加の工夫がなされている例が多く見られた

他社の事例などを調査した上で、Kyashでは以下のような要件を満たすようなproto管理レポジトリを作ることにしました。

  • もともとKyashではレポジトリ名に platform という語彙を使っていたので、事例にならって platform-proto という名称にする
  • コード生成、lint, formatをCI上で実行する
  • ディレクトリ構成は言語やサービスごとにわけるようにする
  • 外部ベンダから提供されるprotoなどを考慮して、lintなどを通さないディレクトリも必要

これらの要件を満たせるように各種ツールやディレクトリ構成の検討を行いました。

ディレクトリ構成について

Kyashで管理するprotoのディレクトリ構成は以下のようにしました。

  • platform-proto直下のディレクトリは配置されるファイルの種類を表す( protogo など)
  • その次のディレクトリはサービス名を表す
  • サービスはそれぞれバージョン管理用のディレクトリ(v1など)を配下にもつ

架空の example サービスを例にtreeを取ると以下のようになります。

platform-proto
│
~
├── doc
│   └── example
│       └── v1
│           └── doc.md
├── go
│   └── example
│       └── v1
│           ├── example.pb.go
│           └── example_grpc.pb.go
└── proto
    └── example
        └── v1
            └── example.proto

また、多くの外部ベンダと接続するKyashならではの特徴として、外部のサービスからprotoが提供されることも考慮する必要があります。外部サービスから提供されたprotoは third_party というディレクトリに配置します。

外部サービスから提供されたproto定義は、Kyashが採用しているlintに準拠しないメッセージ名やパッケージ名などを利用している場合があります。
lintを通すためにパッケージ名を変更すると、gRPCのHTTP/2上のpathが変わってしまいリクエストできなくなってしまうため、 これらのproto定義へのlintは無視することにしました。

また、外部サービスが提供したprotoファイルをそのまま利用することが難しい場合、Kyash側で改変する場合があります。典型的には option go_package などの出力先の指定がされていない場合などです。
その場合は以下のように third_party というディレクトリを挟み、以下のような規則でディレクトリに配置します。

  • 改変元のprotoを platform-proto/proto/third_party/{service_name}/original_v1 に配置。このファイルはコード生成対象にならず、あくまで外部ベンダから連携された生のprotoファイルを残す意味で管理する
  • 改変後のprotoを platform-proto/proto/third_party/{service_name}/v1 に配置

架空の example サービスを例にtreeを取ると以下のようになります。

platform-proto
│
~
├── doc
│   └── third_party
│       └── example
│           └── v1
│               └── doc.md
├── go
│   └── third_party
│       └── example
│           └── original_v1
│           │  ├── example.pb.go
│           │  └── example_grpc.pb.go
│           └── v1
│               ├── example.pb.go
│               └── example_grpc.pb.go
└── proto
    └── third_party
        └── example
            └── v1
                └── example.proto

利用ツールの検討

満たしたい要件から照らし合わせて利用ツールを選定します。

Goのコードについて

まずはGoのコード生成についてです。
gRPCのquick start では

が紹介されていました。最終的にはこれらのツールを利用しました。

念のため他にも利用できそうな実装を調べたところ、protobufメッセージのコード生成としていくつか利用できそうなツールがありましたが、今回は利用を見送っています。

  • https://github.com/golang/protobuf
    • 過去に利用されていた実装で、新しく実装し直したものがquick startで紹介されていた https://google.golang.org/protobuf/cmd/protoc-gen-go という関係性
    • README でも以下のように言及されている
      • It has been superseded by the google.golang.org/protobuf module, which contains an updated and simplified API, support for protobuf reflection, and many other improvements. We recommend that new code use the google.golang.org/protobuf module.`

    • 新規に作る場合は利用しないでよい
  • https://github.com/gogo/protobuf
    • 他の実装よりMarshal/Unmarshalのコストが低い特徴がある。成果物をざっと見た感じだと、型解決などをコード生成時に静的に行うことでリフレクションなしでMarshal/Unmarshalできて他の実装と比べてベンチマークがよい!ということのよう
    • tidbやetcdなどの採用例がある
    • すこしでもMarshal/Unmarshalのコストを下げたい基盤系のサービスにはとても向いていそうな印象
    • 今回の場合はそこまで要件が厳しいわけではないので、公式で紹介されているものを使ったほうが良いだろうということで見送り

documentの生成

他社事例を調べている中で https://github.com/pseudomuto/protoc-gen-doc というドキュメントの生成ツールを見つけました。
調べても実用的な似たツールは他にはなかったし、あって困ることはないだろうということで採用しました。

ドキュメントのリンクを用いてチームメンバーでコミュニケーションができるので、今のところ採用して良かったなと思ってます。

ただ、Goにおける option go_package や他の言語における同様の設定のようにprotoファイル側で出力先の設定ができないので、今回やろうとしているディレクトリ構成に合わせた platform-proto/doc/example/v1/doc.md のような出力をすることがそのままだと難しかったです。

そのため、サービスのディレクトリごとにdocument生成を実施し、それぞれをdoc配下に配置するようなシェルを別途書いてます。

linter, formatter

linterについては、各種linterを比較して https://github.com/uber/prototool を採用しようとしていたのですが README

Update: We recommend checking out Buf, which is under active development. There are a ton of docs for getting started, including for migration from Prototool.

との記載があり代わりにbufを使うように推奨されているようだったので https://github.com/bufbuild/buf を採用しました。lint対象から third_party ディレクトリを除外するような設定をして利用しています。

formatterとしては https://christina04.hatenablog.com/entry/protobuf-formatter を参考にclang-formatを利用しました。この部分はチームの別のメンバーが調査やCIへの追加をしてくれました

CI上でのコード生成について

CIにはgithub actionsを使っています。

手元でもコード生成を実施できるようにするため、処理自体はシェルとして書いて、github actions上でそれを呼び出すような作りにしています。

最終的なコード生成のシェルスクリプトでは、

  • third_party配下は配置のルールが多少異なることを考慮したうえで、コード生成対象のサービスのprotoファイルのディレクトリを取得
  • それぞれのprotoファイルのディレクトリに対応するgoやdocの成果物がすでに存在していれば削除
  • protocを使ってgoとdocを生成

のような処理をしています。

今後 https://github.com/pseudomuto/protoc-gen-doc のように出力先をprotoファイル側で選べないツールを採用する可能性もあるので、そのようなツールを追加しやすいようにprotoファイルのディレクトリごとにコード生成するような書き方で統一しています。

実際のスクリプトは以下です。 scripts/codegen.sh に配置されています。

#!/bin/bash

set -eu
cd "$(dirname "$0")/.."

KYASH_PROTO_DIRS=$(find ./proto/*/v* -type d)
THIRDPARTY_PROTO_DIRS=$(find ./proto/third_party/*/v* -type d)
PROTO_DIRS=$(echo -e $KYASH_PROTO_DIRS"\n"$THIRDPARTY_PROTO_DIRS)
for PROTO_DIR in $PROTO_DIRS; do
    DOC_DIR=$(echo $PROTO_DIR | sed -e 's/proto\(.*\)$/doc\1/g')
    GO_DIR=$(echo $PROTO_DIR | sed -e 's/proto\(.*\)$/go\1/g')
    rm -f $DOC_DIR/doc.md $GO_DIR/*.pb.go
    if [ ! -e $DOC_DIR ]; then mkdir -p $DOC_DIR; fi
    protoc --proto_path=./proto \
    --go_out=. \
    --go_opt=module=github.com/Kyash/platform-proto \
    --go-grpc_out=. \
    --go-grpc_opt=module=github.com/Kyash/platform-proto \
    --doc_out=$DOC_DIR \
    --doc_opt=markdown,doc.md \
    ${PROTO_DIR}/*.proto
done;

CIからのコードpushに関しては、以下のようにコード生成結果とHEADとの差分をとって、差分が存在すれば最新コミットのuser情報でコード生成結果をcommit、pushしています

- name: CommitAndPush
  run: |
    git add -N .
    if ! git diff --exit-code --quiet
    then
      git config --local user.email "$(git log --format='%ae' HEAD^!)"
      git config --local user.name "$(git log --format='%an' HEAD^!)"
      git add .
      git commit -m "Generate code using protobuf"
      git push origin HEAD:${GITHUB_REF#refs/heads/}
    fi

まとめ

Kyashではマイクロサービス間の通信に一部gRPCを利用しており、そのproto管理のために社内で汎用的に使うためのplatform-protoというレポジトリを構築しました。 platform-protoの特徴としては

  • CI上でコード生成/lint/format/doc生成が行われる
  • 外部ベンダから提供されたproto管理のためにthird_partyディレクトリがある

などがあります。

このレポジトリを整理したことで、今後新規サービスでgRPCを採用する障壁が下がり、いろいろなチームがgRPCでの通信を選択しやすくなるとよいなと思います。
余談ですが、platform-protoの仕組みを整えてすぐにVisa、QUICPayの決済基盤の開発を担当しているPaymentチームが早速開発に使ってくれました!作った仕組みがいろいろなチームの役に立っていくのをみるのはとてもうれしいですね...!

最後になりますが、Kyashではエンジニアを募集中です!Fundsチームは銀行やコンビニなどの外部ベンダと接続し、資金移動業ならではの観点を踏まえて残高への入出金を管理しています! ご興味がありましたらぜひこちらをご確認ください。