Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

Protocol Buffersの定義ファイルのチェックはBuf一択でよいのでは?

Retty Advent Calendar 2022 Part1の5日目は、Protocol Buffersの定義ファイルに対してlintやフォーマット、破壊的変更のチェックを Buf でする方法を紹介します。

Part1:

adventar.org

Part2:

adventar.org

はじめに

Rettyではマイクロサービス化を進めており、サービス間の通信の大半にgRPCを利用しています。
gRPCとは、ローカルから別の場所(リモート)にある関数を呼び出して実行する仕組みであるRemote Procedure Call(RPC)を実現するためにGoogleが提案・開発したプロトコルで、この関数の定義や引数、戻り値をProtocol Buffersとよばれる言語で定義します。

Protocol Buffersの定義ファイルの拡張子が proto なので以降protoファイルと記述します。

Rettyではprotoファイルの書き方のルールをドキュメント化していましたが、linterやフォーマッタによるファイルチェックをしていないため細かいところは人によって自由に定義できる状態でした。
そこでprotoファイルのlintやフォーマットのチェックができるBufを導入することで書き方に統一性が出るようにしたので、この記事ではBufによるファイルチェックについて紹介したいと思います。

Bufとは

Bufはprotoファイルのメンテナンス課題を解決するためのユーザーフレンドリーなツールを提供することを目的としていて、以下の機能を提供しています。

  • protoファイルのチェック
    • lint
    • フォーマット
    • 破壊的変更のチェック
      • Protocol Buffersを変更した際に前方・後方互換性を保っているかのチェック
  • protoファイルからGoやJavaなどのコードを生成
  • protoファイルをアップロードするとドキュメントやGoやJavaなどのコードを生成できるSaaSプラットフォームを提供

Buf1つでコードチェック、コード生成、コード配信ができるなんて有能ですね。
Bufが登場する前はlintは protoc-gen-lintprototool を使い、フォーマットには clang-format を使って、 protoc でコードを生成して…など各用途で使用するツールが違い管理が大変だったでしょう。

今回はBufでprotoファイルに対してlintやフォーマット、破壊的変更のチェックする方法について紹介します。

Bufでprotoファイルをチェックする

事前準備

Bufでprotoファイルをチェックするために、まずはprotoファイルのリポジトリの用意とbuf CLIをインストールとセットアップをします。

リポジトリ(example-proto)の初期構成はただ直下にprotoファイルをまとめるディレクトリを作っただけです。

example-proto
└── proto

次にbuf CLIをインストールしましょう。 今回はHomebrewでインストールしますが、GitHubからバイナリをダウンロードしたりGoからビルドする方法も紹介されています。

brew install bufbuild/buf/buf

buf CLIのインストールができたのでexample-protoでprotoファイルのチェックをするためのセットアップをします。
今回はprotoファイルをまとめるためにprotoディレクトリを作ったので以下の内容で buf.work.yaml をroot直下に用意しましょう。

version: v1
directories:
  - proto

buf.work.yamldirectories で指定したことでprotoディレクトリがワークスペースとして認識されたので、さらにBufのモジュールとして認識させるために以下のコマンドを実行します。

cd proto
buf mod init

これで buf.yaml がprotoディレクトリに生成されます。buf CLIの各種コマンドは生成された buf.yaml があるディレクトリ配下すべてのprotoファイルを対象として実行されます。

最後に肝心のprotoファイルが用意されていないのでprotoディレクトリにhelloworldディレクトリを作って、そこに以下の内容で helloworld_service.proto を用意します。

syntax = "proto3";
package helloworld;
message HelloQuery {
  string name = 1;
}
message HelloResponse {
  string msg =   1;
}


service Greeter {
  rpc SayHello(HelloQuery) returns (HelloResponse);
}

最終的にこのようなディレクトリ構成になれば、buf CLIhelloworld_service.proto ファイルのチェックができるようになりました。

example-proto
├── proto
│  ├── helloworld
│  │  └── helloworld_service.proto
│  └── buf.yaml
└── buf.work.yaml

Bufでprotoファイルにlintをかける

buf lint コマンドでprotoファイルに対してlintをかけられるので、実行してみましょう。

proto/helloworld/helloworld_service.proto:2:1:Package name "helloworld" should be suffixed with a correctly formed version, such as "helloworld.v1".
proto/helloworld/helloworld_service.proto:11:9:Service name "Greeter" should be suffixed with "Service".
proto/helloworld/helloworld_service.proto:12:16:RPC request type "HelloQuery" should be named "SayHelloRequest" or "GreeterSayHelloRequest".
proto/helloworld/helloworld_service.proto:12:37:RPC response type "HelloResponse" should be named "SayHelloResponse" or "GreeterSayHelloResponse".

なにやら色々と怒られました。1つ1つ見ていきましょう。

proto/helloworld/helloworld_service.proto:2:1:Package name "helloworld" should be suffixed with a correctly formed version, such as "helloworld.v1".

これは後方互換性を担保するためにパッケージ名にバージョンを指定する、という ルール に引っかかっているため怒られています。
なので該当部分を下記のように変更してもう一度 buf lint を実行してみましょう。

proto/helloworld/helloworld_service.proto:2:1:Files with package "helloworld.v1" must be within a directory "helloworld/v1" relative to root but were in directory "helloworld".

今度は違う理由で怒られました。
これはパッケージ名とディレクトリ構造を合わせることでメンテナンス性をあげるためのルールなので、ディレクトリ構造を以下のように変えます。

example-proto
├── proto
│  ├── helloworld
│  │  └── v1
│  │     └── helloworld_service.proto
│  └── buf.yaml
└── buf.work.yaml

この状態でもう一度 buf lint を実行すると、パッケージ名に関するルールを鎮めることができました。

lintで指摘されている他の箇所も同様に下記のように変えていきましょう。

この状態で buf lint を実行すると特に指摘されずにlintが通ったことが確認できます。

初期状態ではすべてのlintルールを適用するするようになっていますが、buf.yaml ファイルで無視するルールやディレクトリ・ファイル毎にルールを適用しないという設定ができます。また、protoファイルに直接コメントをつけることで、そのルールを無視することもできます。
詳しくは公式を参考にしてください。

Bufでprotoファイルのフォーマットをする

改めてlintルールを適用したprotoファイルの中身を見てみましょう。

syntax = "proto3";
package helloworld.v1;
message SayHelloRequest {
  string name = 1;
}
message SayHelloResponse {
  string msg =   1;
}


service GreeterService {
  rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
}

SayHelloResponseのmsgフィールド定義部分に余計なスペースが入っていたり、GreeterService定義部分の上に2つ改行が入っていたりして見辛いのでフォーマットを統一したいですね。

buf CLIでは format というサブコマンドが用意されているので実行してみましょう。

buf format -w

実行後のファイルは下記のように必要な改行がされていたり、逆に不要な改行を削除したり、不要なスペースを削除してくれたりしてます。便利ですね。

Bufで破壊的変更をチェックする

Protocol Buffersは前方・後方互換性を保ちつつ定義する必要があります。
とはいっても間違って修正してしまうこともあると思うので、そういった互換性を破壊するような変更をチェックできる機能をBufは提供しています。

proto/helloworld/v1/helloworld_service.proto で定義したSayHelloRequestのnameフィールドの名前を変更します。

今回はGitで管理しているため、フィールド名変更前のコミットと比較して破壊的変更のチェックをしてみたいと思います。

buf CLIで破壊的変更をチェックするには breaking サブコマンドを利用します。
breaking サブコマンド実行時には against オプションで比較する対象を指定する必要があり、Gitリポジトリのブランチやタグと比較できたりtarなどでアーカイブされたものと比較できたりします。
詳しくは公式を参考にしてください。

今回は変更前のコミット(HEAD)と比較したいので buf breaking --against '.git#ref=HEAD' を実行してみると、以下のようにフィールド名が変更されて破壊的変更が起きていることをチェックしてくれました。

proto/helloworld/v1/helloworld_service.proto:6:10:Field "1" on message "SayHelloRequest" changed name from "name" to "namee".

おわりに

この記事ではBufによるprotoファイルのチェックについて紹介しました。
buf CLI1つでlint・フォーマット・破壊的変更のチェックができるので、リポジトリに余計なファイルが入り込まず管理しやすくなりました。
今回はbuf CLIによるprotoファイルチェックでしたが、GitHub Actionsで使えるactionも公開しているのでCIに組み込みやすいです。

Bufを開発したところは他にgrpc-goに代わるConnectを開発したりprotocol buffersのLanguage Serverを公開したりと、gRPC周りのツールを提供しているので定期的にブログを見ていると楽しいですね 👍