Protocol Buffers からのコード生成を Buf へ移行した背景

2023-03-10

Overview

Belong では API 間の通信に gRPC を用いています。これまでは protoc を用いてリソースの生成を行っていましたが、Buf が v1 の安定版となった事をきっかけに gRPC を含めた Protocol Buffers からの各種リソースの生成を Buf を利用する形へ移行しました。
本記事では Buf がそもそも何なのかという点に触れつつ、移行するメリットやモチベーションなどの背景を紹介します。

Background

Belong では API 間の通信に gRPC を用いているため、API の定義には Protocol Buffers (以降 Protobuf) を利用します。 このとき、 gRCP の Server/Client のコードであったり、HTTP で通信を可能にする grpc-gateway や OpenAPI の定義は Protobuf から自動生成します。

Protobuf generation

Protobuf からリソースを生成する場合、公式 (Protocol Buffers Tutorial) では protoc という Protobuf のコンパイラと各プログラミング言語やリソースを生成するためのプラグインを用います。 Belong も元々 protoc を利用しており、Buf を Linter や Breaking Change の検知のために利用していました。
このとき、Buf の利用開始当時は v1beta1 という安定版ではない位置づけの状態だったため、必要なら依存を取り除ける形で利用していましたが、 2022 年に Buf が v1 として安定版に至ったことをきっかけとして、コード生成メカニズムの簡易化を行うため一つの統合的なツールとして Buf を用いる形にしました。

What is Buf?

まずそもそも Buf は何なのかという点について少し深堀りします。 Buf は Protobuf を扱うためのコマンドラインツールで、以下のような機能を持っています。

  • プラグインを利用したコードの生成
  • Linter
  • Formatter
  • Breaking change の検知
    • Protobuf を変更した時に前方・後方互換性があるかの確認

protoc を用いる場合、linter や format 機能は外部のプラグインを用いる必要がありますが、Buf ではコマンドラインツールひとつで上記のような一般的に必要だと思われる機能が揃います。 また、Buf はコード生成のパフォーマンスも protoc より良いようです。

Buf の使い方は Tour of Buf を読みつつ実際に手を動かすとわかりやすいですが、一般設定ファイルの buf.yaml とプラグインを用いたコード生成設定ファイル buf.gen.yaml を用います。buf.gen.yaml に利用するプラグインと関連設定を記述すると Go, Java, Python など様々なリソースを Protobuf から生成出来ます。 下記が それぞれの設定ファイルの例です。

buf.yaml

Lint の詳細設定や breaking Change の設定などを含む。

version: v1
lint:
  use:
    - DEFAULT
  except:
    - FILE_LOWER_SNAKE_CASE
  ignore:
    - bat
    - ban/ban.proto
  ignore_only:
    ENUM_PASCAL_CASE:
      - foo/foo.proto
      - bar
    BASIC:
      - foo
  enum_zero_value_suffix: _UNSPECIFIED
  service_suffix: Service
  allow_comment_ignores: true
breaking:
  use:
    - WIRE
    - FILE_NO_DELETE

buf.gen.yaml

どの Plugin を利用してコードを生成し、どこに配置するかなどの設定を含む。

version: v1
plugins:
  - plugin: go
    out: gen/go
    opt: paths=source_relative
    path: custom-gen-go
    strategy: directory
  - plugin: java
    out: gen/java
  - plugin: buf.build/protocolbuffers/python:v21.9
    out: gen/python

Motivation of migration

ここでは、なぜ protoc を利用するコード生成から Buf を用いる形にしたかを説明します。

Dependency management

先に触れたとおり Buf は早い、簡単、たくさん機能入りというわかりやすい利点がありますが、個人的に特に気に入ったポイントとしては依存管理の方法です。

Protobuf を利用する場合、外部で定義されたものを利用する場合があります。 よくある例としては、 googleapisgoogle/type/date.proto で日付の定義を利用したり、grpc-gatewayprotoc-gen-openapiv2/options/annotations.proto で OpenAPI 用のアノテーションを記述することです。

import とアノテーションの例

必要な依存を import し、HTTP 用のリソースを生成するためのアノテーションを行っている。

import "google/api/annotations.proto";
import "google/type/date.proto";

// Sample service to say hello.
service HelloService {
  rpc Hello(HelloRequest) returns (HelloResponse) {
    option (google.api.http) = {
      get: "/api/v1/hello"
    };
  }
}

protoc でのコード生成を行う場合、これらの依存している proto ファイルを何らかの方法でローカルに保持する必要がありました。 最も簡単な方法だと手元にコピペをし自身で管理する形ですが、もう少し洗練された方法は git の submodule 機能を用いてサードパーティーの依存を submodule として管理する方法です。 後者はバージョンの更新が git のコマンドで行え、特定の tag や、maser ブランチの最新のものの取得 (tag がないリポジトリがある) が比較的行いやすいです。

Buf の場合、外部 proto ファイルの依存は buf.yaml ファイルで管理でき、依存先とそのバージョン (tag やコミットハッシュ) の指定が可能です1。 この依存は buf.lock ファイルで管理され、バージョンを指定しない場合は最新のものが取得されます。
これにより、ローカルで煩雑な外部依存先の管理を行う必要がなくなり、Buf 設定ファイル以下で管理ができます。

依存の管理を行う buf.yaml

version: v1
name: buf.build/acme/petapis
deps:
  - buf.build/acme/paymentapis # The latest commit.
  - buf.build/acme/pkg:47b927cbb41c4fdea1292bafadb8976f # The '47b927cbb41c4fdea1292bafadb8976f' commit.
  - buf.build/googleapis/googleapis:v1beta1.1.0 # The 'v1beta1.1.0' tag.

(https://docs.buf.build/configuration/v1/buf-yaml#deps)

protoc を用いる場合、複数のリソース生成は Makefile や Shell Script を用いて各種依存の準備や生成コマンドの実行を行う形になりますが、 Buf では生成物が yaml で管理ができるためわかりやすく必要な設定などの記述量も少なくなります。 このような Buf の簡潔な設定方法や、ツールセットが揃っていること、依存管理が容易になるなどの利点から、protoc から Buf をベースとした生成の仕組みへ移行することを決めました。

Remote plugin and package

プラグインの導入が簡単になることも Buf を利用する理由のひとつです。

Buf は BSR (Buf Schema Registry) からプラグインを取得してリソースを生成する Remote Plugin 機能が使えます。 この Remote Plugin で利用可能なプラグインは Buf チームによって配布され、すぐに使用できることが検証されています。

buf.gen.yaml using remote plugin

version: v1
plugins:
  # Use protoc-gen-go at v1.28.1
  - plugin: buf.build/protocolbuffers/go:v1.28.1
    out: gen/go
  # Use the latest version of protoc-gen-go-grpc
  - plugin: buf.build/grpc/go
    out: gen/go

また、go mod や npm などの各言語の依存管理の仕組みを用いて Buf チームが BSR でサポートしているもの以外のプラグインを利用できる Remote Package という仕組みあります。

これらの仕組みにより、プラグインの導入が以前より簡単になっています。

こちらは余談となりますが、私が Buf v1 への移行に取り掛かった時期は 2022 年半ばで、当時の Remote Plugin はアルファバージョンという位置付けで仕組みが異なっていました。
当時は buf.build/protocolbuffers/plugins/go の形でプラグインのテンプレートを BSR のリポジトリ毎に持ち、buf.gen.yaml で nameplugin の代わりに remote としてプラグインを定義してビルド時に利用していました。 プラグインは BSR のリポジトリ毎に各々アップロードや利用が出来たのですが、現在この仕組みは deprecated になり、 BSR から取得できるプラグインは Buf チームがサポートするもののみになったようです。
この理由はRemote Plugin の仕組みが v1 になるアナウンスで触れられていますが、プラグイン自体を作るのが手間であったり、複数の似たようなプラグインが乱立して混乱を招いたようでした。 特に後者の理由は私も Buf v1 移行のための調査当時に気になっており、GitHub で見つかるプラグインを BSR に複数の人が Push している状態でバージョンもまばらで、どれを使用するべきかわかりづらい状態でした。 なので、現在の Buf や Protobuf のエコシステムの公式 (に近い) のプラグインは Buf チームが BSR でサポートし、独自のものは各言語のエコシステムを利用して取得し実行するという形はわかりやすくて良いと個人的にも思います。

Conclusion

本記事では Belong が Protobuf からのリソースの生成を protoc から Buf に移行した話を紹介しました。 Buf はコード生成だけでなく、Linter や Formatter、Braking Change の検知機能も備えており、統合的なコマンドラインツールとして扱えます。
また、Buf を用いるメリットとして依存管理やプラグインの導入が簡単になることにも触れました。

移行の方法には触れませんでしたが、Belong は元々 protoc と buf v1beta1 を併用していたので Buf v1 への移行はとても簡単でした。 移行ガイド が用意されており、基本的には buf beta migrate-v1beta1 コマンドを実行すると buf.build.yaml などの必要な設定ファイルが生成されます。

Belong では Go や Python を利用して API を開発するバックエンドエンジニアを始めとして様々なポジションのエンジニアを継続的に募集しています。 今回の記事で触れた Protobuf を定義し gRPC を用いた API 開発の経験がある方、そういった開発経験はないがやってみたい方など色々なバックグラウンドの方を受け入れられますので、 もし Belong のエンジニアリング部門に興味を持っていただけたら以下の URL をご参照いただきお声がけいただけたら幸いです。

https://entrancebook.belonginc.dev/

Footnotes

  1. you should depend on the latest commit whenever possible.公式ページに書いてある通り基本的には最新版を利用するのが良い。