BufやConnectといった最近のgRPC 開発について調べるメモ
公式ドキュメントを読む
イントロ
ProtobufはREST/JSONで開発するAPIよりも多くの利点あるがProtobufを使用して開発するにはなかなか楽ではない。Bufは開発者がアプリケーションロジックに集中できるようにProtocbufまわりの関心ごとを引き受けツールとして公開してくれている。具体的にはBuf CLI
とBuf Schema Registory
がある。
従来のProtocbufを使用したAPI開発には以下のような課題がある。
- API設計に一貫性がない。(あんまりピンときてない)
- 依存関係 ファイル間の依存関係の解決にコピペでやるしかなくバグを引き起こしやすい。これはnpmなしでJavaScriptを書くようなもの
- 前方互換性と後方互換製を強制できてない
- スタブの配布が難しい 生成されたコードを配布するか、全てのクライアントがprotocを独立して実行する必要がある。これはprotocbufを採用した開発のハードルを一気に上げる
- ツールのエコシステムが限られている
Buf CLI
以下のような特徴がある
- 高性能なProtocbufコンパイラ
- Linter機能
- 互換性を強制する変更検出機能
- 設定可能なテンプレートに基づいてprotocプラグインを呼び出すコードジェネレーター
BSR(Buf Schema Registry)
BSRはProtobuf APIのホスト型SaaSプラットフォームのこと。BSRはProtobufエコシステムに依存関係管理を導入する。また、リモートプラグインやSDK生成の機能によりクライアントコードの生成に関する工程をシンプルにする。
install
Homebrew, npm, dockerなどがあるが今回はHomebrewでインストール。
brew install bufbuild/buf/buf
% buf --version
1.29.0
Buf CLI Tutorial Tour
ドキュメント記載のリポジトリをクローンする
% git clone https://github.com/bufbuild/buf-tour
% cd buf-tour/start/getting-started-with-buf-cli/proto
% tree .
.
├── google
│ └── type
│ └── datetime.proto
└── pet
└── v1
└── pet.proto
bufはbuf.yaml
が設定ファイルとなっている。以下のコマンドでファイルを生成する。
% buf mod init
% cat buf.yaml
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
buf.yaml
はprotoファイルがある場所に配置したほうがいい。protoファイルからprotc互換のコード生成を実行するのにbuf.gen.yaml
をprotoディレクトリがある場所に配置する。buf.gen.yaml
はbuf generate
コマンドが指定されたモジュールでどのようにprotocプラグインを実行するかを制御する。
% touch buf.gen.yaml
% cat <<'EOF' > buf.gen.yaml
version: v1
managed:
enabled: true
go_package_prefix:
default: github.com/bufbuild/buf-tour/gen
plugins:
- plugin: buf.build/protocolbuffers/go
out: gen
opt: paths=source_relative
- plugin: buf.build/connectrpc/go
out: gen
opt: paths=source_relative
EOF
以下のコマンドでコードを自動生成
buf generate proto
protocの複雑なコマンド実行をしなくてよいのがbufの強力なところ。bufの場合はbuf.gen.yaml
に全て記載される。
このようにbufはprotocの代替となる優れた簡素化されたコンパイラだが単なるProtobufコンパイラではない。buf CLIはbuf lint
コマンドでlint機能を提供している。
% buf lint proto
proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".
proto/pet/v1/pet.proto:42:10:Field name "petID" should be lower_snake_case, such as "pet_id".
proto/pet/v1/pet.proto:47:9:Service name "PetStore" should be suffixed with "Service".
上記の実行結果はいくつかlintルールに引っかかっている。これはbuf.yaml
に記載されたDEFAULT
ルールに基づく。
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
FIELD_LOWER_SNAKE_CASE
、SERVICE_SUFFIX
のルールを修正していく
syntax = "proto3";
package pet.v1;
...
message DeletePetRequest {
- string petID = 1;
+ string pet_id = 1;
}
message DeletePetResponse {}
-service PetStore {
+service PetStoreService {
rpc GetPet(GetPetRequest) returns (GetPetResponse) {}
rpc PutPet(PutPetRequest) returns (PutPetResponse) {}
rpc DeletePet(DeletePetRequest) returns (DeletePetResponse) {}
}
残りの1つの警告を見ていく
proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".
これはpackage名のsuffixにバージョンを指定する必要がありそうだが、datetime.proto
はプロジェクトファイルではなくgoogleapisが提供している依存関係である。そのため、lintを通すためにパッケージ名を修正することもできないためlintの対象外とする
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
+ ignore:
+ - google/type/datetime.proto
以下のコマンドを実行して無視したいルールの最小セットを出力することもできるので、一旦これをbuf.yaml
に記載して、後で直すということもできる。ただし、経験上これをやって後で直すことはない。
% buf lint proto --error-format=config-ignore-yaml
version: v1
lint:
ignore_only:
PACKAGE_VERSION_SUFFIX:
- google/type/datetime.proto
次にbuf breaking
コマンドによる変更検知の機能について。protobufの破壊的変更がある場合、影響範囲がそこまで広くなければやってしまったほうがいいとも考えられるが、影響範囲が広かったりすると破壊的変更はなるべく避けたいと思うかもしれない。いずれにせよ、そういった互換性のない変更を検知することでレビュー負荷はかなり下がります。
変更検知の単位はFILE
やPACKAGE
、WIRE
、WIEW_JSON
とあるがデフォルトはファイル単位で検知するFILE
です。
コマンドを実行して変更検知をためすためにpet.protoを以下のように修正する。
message Pet {
- PetType pet_type = 1;
+ string pet_type = 1;
string pet_id = 2;
string name = 3;
}
以下のコマンドで変更値。比較しているのはgitディレクトリ内のファイルを指定することで以前のバージョンとの比較を実施している。
% buf breaking proto --against "../../.git#subdir=start/getting-started-with-buf-cli/proto"
proto/pet/v1/pet.proto:1:1:Previously present service "PetStore" was deleted from file.
proto/pet/v1/pet.proto:18:3:Field "1" on message "Pet" changed type from "enum" to "string".
proto/pet/v1/pet.proto:35:28:Field "1" with name "pet_id" on message "DeletePetRequest" changed option "json_name" from "petID" to "petId".
proto/pet/v1/pet.proto:35:35:Field "1" on message "DeletePetRequest" changed name from "petID" to "pet_id".
以下のようなGoのプログラムを動かしてみる。
package main
import (
"context"
"fmt"
"log"
"net/http"
petv1 "github.com/bufbuild/buf-tour/gen/pet/v1"
"github.com/bufbuild/buf-tour/gen/pet/v1/petv1connect"
connect "connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
const address = "localhost:8080"
func main() {
mux := http.NewServeMux()
path, handler := petv1connect.NewPetStoreServiceHandler(&petStoreServiceServer{})
mux.Handle(path, handler)
fmt.Println("... Listening on", address)
http.ListenAndServe(
address,
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
}
// petStoreServiceServer implements the PetStoreService API.
type petStoreServiceServer struct {
petv1connect.UnimplementedPetStoreServiceHandler
}
// PutPet adds the pet associated with the given request into the PetStore.
func (s *petStoreServiceServer) PutPet(
ctx context.Context,
req *connect.Request[petv1.PutPetRequest],
) (*connect.Response[petv1.PutPetResponse], error) {
name := req.Msg.GetName()
petType := req.Msg.GetPetType()
log.Printf("Got a request to create a %v named %s", petType, name)
return connect.NewResponse(&petv1.PutPetResponse{}), nil
}
プログラムを起動したらbuf curl
コマンドでリクエストを送ってみる。もうgrpcurlはいらない。
% buf curl \
--schema proto \
--data '{"pet_type": "PET_TYPE_SNAKE", "name": "Ekans"}' \
http://localhost:8080/pet.v1.PetStoreService/PutPet
{}
チュートリアルは以上。
Buf CLIのおかげでprotocもgrpcurlもclang-formatもいらないし、lintと互換性チェックまでできる。Bufすげー
BSR(Buf Schema Registry)のチュートリアルツアー
BSRにProtobufモジュールをpushするためにBufアカウントにサインアップする。BSRにログインしたらトークンを作成する。作成したら以下のコマンドでログインする。
buf registry login
ユーザー名と作成したトークンを入力しログイン。ログインができたらBSRに新たにリポジトリを作成しサンプルのprotobufモジュールをpushする。
リポジトリが作成できたらbuf.yaml
にname
フィールドを追加する。
version: v1
name: buf.build/<USERNAME>/petapis
breaking:
use:
- FILE
lint:
use:
- DEFAULT
以下のコマンドでprotobufモジュールをpushする。
buf push
protobufモジュールをpushすると各ファイルごとにドキュメントとしての役割も果たしますが、モジュール自体を説明するドキュメントはない。これはbuf.md
を作成して再度buf push
することでモジュール自体の説明を加えることができる。GitHubのREADMEのようなもの
次に依存関係の解決方法を見ていく。googleapis
で公開されているprotobufモジュールが用意されているが今までこれはローカルにファイルをコピペして配置していた。これは言うまでもないがモジュールの更新にも追従できずに非常にやっかいだ。
これをBSRを使用することで解決することができる。チュートリアルで用意されているgoogleapis
のコピーしてきたファイルを削除し、buf.yaml
に以下のように依存関係の記載を追記する。
deps:
- buf.build/googleapis/googleapis
追記できたらビルドを実行すると以下のような警告が出る。
% buf build
WARN Specified deps are not covered in your buf.lock, run "buf mod update":
- buf.build/googleapis/googleapis
pet/v1/pet.proto:5:8:read google/type/datetime.proto: file does not exist
これはbuf.lock
ファイルにない依存関係が指定されたことで警告が出ています。buf.lock
ファイル自体ないので以下のコマンドを実行する。
buf mod update
これでbuf.lock
ファイルが作成される。これでビルドが通るにようになる。
ビルドは実際のファイルとして出力されるわけではなくメモリ上に保存される。このビルドイメージはこのあとのコード生成などに使われることになる。
次にコード生成。googleapis
の依存関係をBSRで公開されているモジュールに置き換えたのでbuf.gen.yaml
でgoogleapis
は指定する必要がある。
version: v1
managed:
enabled: true
go_package_prefix:
default: github.com/bufbuild/buf-tour/gen
except:
- buf.build/googleapis/googleapis
plugins:
- plugin: buf.build/protocolbuffers/go
out: gen
opt: paths=source_relative
- plugin: buf.build/bufbuild/connect-go
out: gen
opt: paths=source_relative
これでコード生成すると依存関係はBSRで管理されているため、googleapis
から生成されたコードは含まれない。BSRに公開したモジュールは普通にgo get
できる。
go get buf.build/gen/go/junichi-y/petapis/protocolbuffers/go
go get buf.build/gen/go/junichi-y/petapis/connectrpc/go
クライアントのコードは以下のような感じ
package main
import (
"context"
"log"
"net/http"
// Replace <USERNAME> with your BSR username if username isn't present
"buf.build/gen/go/junichi-y/petapis/connectrpc/go/pet/v1/petv1connect"
petv1 "buf.build/gen/go/junichi-y/petapis/protocolbuffers/go/pet/v1"
connect "connectrpc.com/connect"
)
func main() {
client := petv1connect.NewPetStoreServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
res, err := client.PutPet(
context.Background(),
connect.NewRequest(&petv1.PutPetRequest{
PetType: petv1.PetType_PET_TYPE_SNAKE,
Name: "Ekans",
}),
)
if err != nil {
log.Println(err)
return
}
log.Println(res.Msg)
}
チュートリアルは以上
BSRを使うことでprotoc-gen-docもクライアントの人にprotocによるコード生成をお願いしたりする必要がなくなる。Bufすげー
Buf CLI
protocを使用したコード生成の課題の一つにprotocやプラグインとの連携の複雑さがある。さまざまなコンパイラやプラグインのバージョンが複雑に絡み合い1台のマシンで安定した環境をローカルに管理・維持することは困難である。チームメンバー間でこれを共有するために酷いbashファイルやMakefileを利用することになる。Buf CLIはこれを解決する
- protocより2倍速くコンパイルできる
- ビルド設定を一回すればいい。複雑なコマンドやオプションを覚える必要はない
- protocよりも柔軟。protocは
.proto
ファイルしか受け付けない。
さらに、BufはProtobufファイルからクライアントコードを生成する際の多くのフラストレーションを軽減する。
- リモートプラグイン ローカルにプラグインを用意する必要はない。
- マネージドモード
.proto
ファイルからユーザーや言語固有のProtobufオプションを削除することができる。 - 生成されたSDK BSRを使用することでコードを生成する必要はない。生成されたSDKをnpmなどの依存関係ツールを使用してモジュールを使用することができる。
protoc-gen-go
みたいなローカルプラグインを使う場合、各開発者が自分のマシンに用意することになるので容易に私だけ動かない状態になる。Bufを使うことでBSRに存在するprotocプラグインを使用することができるのでチーム間でのコード生成において一貫性を保てる
以下のような言語固有のoptionをマネージドモードを有効にすることで削除することができる。
syntax = "proto3";
package acme.weather.v1;
option java_multiple_files = true;
option java_outer_classname = "WeatherProto";
option java_package = “com.acme.weather.v1”;
Breakeing Change
protoファイルの破壊的変更検知。protoファイルの互換性を維持するために破壊的変更は基本的に避けた方が良い。ただし、影響範囲がそこまで広くなく、クライアント側とちゃんと連携できているのであれば破壊的変更をしてしまったほうがいい場合もある。いずれにせよ破壊的変更を検知できる仕組みが重要である。
破壊的変更検知は以下の3つのタイミングで実行できる。
- 開発中 エディタと統合することで開発中に早いサイクルで検知できる
- CI/CD GitHub ActionsのようなCI/CDで実行することができる
- BSR BSRにpushする際に破壊的変更を検知できる
Lint
lintをかけるタイミングは2箇所。
- 開発中 エディタ統合
- コードレビュー中 CI/CD(GitHub Actionsなど)
lintのルールはBuf側で用意された以下の3つのレベルを使用することができる。ルールの厳しさは上から下にかけてゆるくなる。
- DEFAULT
- BASIC
- MINIMAL
このルールとは別に以下の2つの制約が用意されている。
- COMMENTS Protobufスキーマのさまざまな部分にコメントを強制する
version: v1
lint:
use:
- DEFAULT
- COMMENT_ENUM
- COMMENT_MESSAGE
- UNARY_RPC ストリーミングRPCを禁止する
lintはローカルのProtobufファイルだけでなくGitリポジトリやBSRなどを指定することもできる。lintのルールはexcept
などを使いカスタマイズすることもできるし、運用していて途中で導入する場合にlintの警告を無視したい場合などにignore
のようなオプションを使うこともできる。
これはbuf.yaml
に以下のような感じで記載する。
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
rpc_allow_same_request_response: false
rpc_allow_google_protobuf_empty_requests: false
rpc_allow_google_protobuf_empty_responses: false
service_suffix: Service
allow_comment_ignores: true
Buf curl
以下のようにJSON形式でパラメーターを指定してリクエストすることができる。
% buf curl \
--data '{"sentence": "I feel happy."}' \
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
{
"sentence": "Feeling happy? Tell me more."
}
デフォルトのRPCプロトコルはConnect。別のプロトコル、gRPC、gRPC-Webを使用するには-protocol
フラグを使用する。
リクエストメタデータは-H
または--header
フラグを使用して指定する。
Buf CLIはデフォルトでサーバーリフレクションを対応していることを期待します。もし、サーバーがリフレクションを対応していなければ--schma
フラグを使用してスキーマを指定することができる。
Build
buf build
コマンドを実行することでProtobufファイルをビルドする。ビルドはbuf.yaml
の設定を考慮して実行される。buf.yaml
の設定項目は以下。
version: v1
build:
excludes:
- foo/bar
Bufの考えではディレクトリごとに分かれ、buf.yamlが配置されている単位でモジュールとみなしている。複数のモジュールを管理するのにBufはbuf.work.yaml
を使用したワークスペースをサポートしている。protocにおいては以下のように複数のディレクトリを-I
オプションで指定しているのと同義である。
protoc \
-I proto \
-I vendor/protoc-gen-validate \
-o /dev/null \
$(find proto -name '*.proto')
buf build
コマンドは以下の処理を行う。
- buf.yaml 設定に従ってすべての Protobuf ファイルを検出する。
- Protobufファイルをメモリにコピーする。
- すべてのProtobufファイルをコンパイルする。
- コンパイル結果を設定可能な場所に出力する(デフォルトは/dev/null)。
デフォルトではコンパイル結果を捨ててしまうが-o
オプションを指定することで以下の形式で出力できる。
- JSON
- text
- binary
buf build -o image.binpb
buf build -o image.binpb.gz
buf build -o image.binpb.zst
buf build -o image.json
buf build -o image.json.gz
buf build -o image.json.zst
buf build -o image.txtpb
buf build -o image.txtpb.gz
buf build -o image.txtpb.zst
BSR(Buf Schema Registry)
BSRはProtobufをチーム内でいい感じに配布するためのレジストリサービス。Docker Hubのようなもの。Protocbufを使用したチーム開発ではクライアントとサーバー間でしばしば食い違う。BSRにProtocbufモジュールを公開しておくことで容易に配布することができるようになった。
また、依存関係の解決も可能となる。現代のプログラミング言語にはいわゆるパッケージマネージャーのようなものが必須であることが多い。JSのnpm、GoのGo モジュール。もうコピペをする必要はない。
そして、これが個人的には嬉しいことだがドキュメントの公開という役割もある。従来であればprotoc-gen-doc
のようなprotocプラグインを使用してドキュメントを自動生成していたがもうそれも不要になる。
Bufでは意味あるProtobufファイルの集まりをモジュールとして扱い、そのルートパスにはbuf.yaml
が配置されることが期待されている。モジュール名はbuf.yaml
に記載される。
version: v1
name: buf.build/acme/weather
nameの値は{Remote}/{Owner}/{Repository}
の形式になる。RemoteはBSRのホスティングサーバーのDNSで常にbuf.build
になる。
モジュールの最小構成は以下。
proto/
├── acme
│ └── pkg
│ └── v1
│ └── pkg.proto
├── buf.lock
├── buf.md
├── buf.yaml
└── LICENSE
BSRを使用する時の認証の仕組みとして
-
BUF_TOKEN
環境変数を使用する。(CI環境推奨) -
.netrc
ファイルを使用する。(開発環境推奨)
がある。いずれにせよBSRによって生成する認証トークンを使用する。認証の優先順位は.netrc
ファイルよりも環境変数の方が上。環境変数による認証は以下のように複数のトークンを含めることができる。
export BUF_TOKEN=${TOKEN1}@{REMOTE1},${TOKEN2}@{REMOTE2},...
開発環境で認証する場合、以下のようにloginコマンドを実行すればいい。
buf registry login
BSRにProtobufモジュールを公開するとしてGitHubのように複数のブランチがないと開発中のモジュールを本番運用中のモジュールに統合せずに公開するのが難しい。
そのため、BSRはブランチ機能をサポートしている。GitHub Actionsでbuf-push-action
を使用すればいい感じにBSRへのモジュール公開をやってくれる。
name: buf-push
on: push # Apply to all pushes
jobs:
push-module:
# Run `git checkout`
- uses: actions/checkout@v3
# Install the `buf` CLI
- uses: bufbuild/buf-setup-action@v1
# Push the module to the BSR
- uses: bufbuild/buf-push-action@v1
with:
buf_token: ${{ secrets.BUF_TOKEN }}
# Push as a branch when not pushing to `main`
branch: ${{ github.ref_name != 'main' }}
公開してあるProtobufモジュールをリモートで参照しコードを自動生成することも可能だが、自動生成後の各言語のコードを直接パッケージ管理ツールで使用することができる。JSであればnpmモジュールとしてだしGoであればGoモジュールとして。
BSRはGitHubのようにタグ管理もサポートしている。以下コマンドでGitのコミットとBSRのモジュールのバージョンを紐づけることができる。
buf push --tag "$(git rev-parse HEAD)"
Protocbufモジュールは他のモジュールを依存関係として追加することができる。これはbuf mod
コマンドを使用したり、buf.yaml
ファイル内に依存関係を追加して管理する。その際に、現状の依存関係を固定し再現するためにbuf.lock
ファイルが作成される。
Buf Studioを使えばweb画面から特定のRPCと対話できる。認証情報もつけれる。
gRPC
BufではないけどgRPCについて今一度確認しておく。
gRPCとはgoogleが2015年に開発したオープンソースのRPC。もともとgoogleが社内で使用していた独自のRPCにHTTP2などを組み合わせて標準化したもの。
そもそもRPCとはRemote Procedure Callの略でクライアント、サーバー間の通信プロトコルの一種でありクライアントからサーバー側で登録されている関数をリモートで呼び出すような技術。RPCは古くから存在しておりさまざまなデータフォーマットで利用されている。例えば、XMLを利用するXML-RPCやJSONを利用するJSON-RPCがある。
こういった、 RPCの技術をどうやって使えるのかというとGoで言うならnet/rpc
といった標準パッケージが容易されており、サーバー側もクライアント側も実装できるようになっている。
ChatGPTにexampleを出してもらった。
GoによるJSON-RPCの実装例
// サーバーサイド
package main
import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
"log"
)
type Args struct {
A, B int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
listener, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, e := listener.Accept()
if e != nil {
continue
}
go jsonrpc.ServeConn(conn)
}
}
// クライアントサイド
package main
import (
"net/rpc/jsonrpc"
"log"
"fmt"
)
type Args struct {
A, B int
}
func main() {
client, err := jsonrpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
args := Args{7, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
}
gRPCもJSON-RPCやXML-RPCと同様、RPCという通信プロトコルの1種であるが特徴としてHTTP2通信を使用している点とデータのシリアライズにProtocol Buffersを使用していることが特徴。厳密には通信プロトコルとデータのシリアライズ方法は他のものに置き換え可能らしいがHTTP2、Protocol Buffersのデフォルトの組み合わせで使用されることが多く、その組み合わせをgRPCとして指すことが多い、ように感じる。
gRPCが流行ったのはGoogleが社内で利用していたマイクロサービスのサービス間通信にgRPCの前身となるようなRPC通信が使用されていて、マイクロサービス自体が流行り出したときに注目されたのかなという印象。マイクロサービスとセットで話題になることが多いがモバイルアプリのクラサバ通信に使われたりするのもよく見かける。
gRPCは今までのRPC通信がテキストベースでデータの通信効率が悪く、バイナリデータを扱いにくかったという課題を解決しており、特にデータの転送効率という面が優れており、無数のマイクロサービス内のサービス間通信にまさに適した通信プロトコルとなっている。
gRPCの開発体験についてスキーマ駆動開発の良さがある。これは現場によっては複数のクライアントとサーバー間でAPIのすり合わせをすることのハードルをかなり下げることにつながっていると思っている。gRPCというよりもProtobufの体験の良さが関係しているともいえる。
いずれにせよ、Protobufを用いたスキーマ駆動の開発はクライアントとサーバー間の摩擦をなくし、いい感じに開発できて個人的には気に入っている。protocの複雑さやドキュメント化の問題もBufが解決してくれたように思える。
ちなみにgRPCはCNCF傘下で開発が進められている。
公式
Protocol Buffers
上述したgRPCで使用されているデータシリアライゼーション。実際は.proto
ファイルで作成されるIDL、protocのようなコンパイラが生成するコード、言語固有のランタイム・ライブラリなどの組み合わせを指す。
ProtobufがサポートしているのはC++, C#, Java, Kotlin, Objective-C, PHP, Python, Rubyの8言語。GoとDartはGoogleがサポートしており、実際はprotcのプラグインとして提供されている。それ以外の言語もGithubリポジトリでカスタムプラグインとして公開されているものがある。
公式
ちなみに、protoファイルでgoogle/protobuf/any.proto
のようなインポートができて、使用できるようになっているがこれはprotobufの標準モジュールとして用意されている。
以下の記事でProtobufがなぜいいのかをかなり深く書いている
Connect
ConnectはBufが開発しているブラウザとgRPC互換のHTTP APIを構築するためのライブラリ群である。Connectは複数のプロトコルをサポートしており、gRPC
とgRPC-web
と独自のConnect
プロトコルをサポートしている。
対応言語は執筆時点でGo, TS,(JS) Swift, Kotlinが対応されており、GoとTSはBuf自体でも本番運用されており安定している。
他の言語サポートにおいてはConnectはHTTP/1.1
でもUnary RPCが動作するように設計されているためcurlやweb標準のfetchでリクエストできるためこれらを利用することもできる。しかし、他のサポート言語のような完全な型安全性は得られない点には気をつけたい。
# curl
curl --header "Content-Type: application/json" \
--data '{"sentence": "I feel happy."}' \
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
# fetch
fetch("https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say", {
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": JSON.stringify({"sentence": "I feel happy."})
})
.then(response => { return response.json() })
.then(data => { console.log(data) })
詳しくはこちら
以下はGoのConnectを使用したサーバー側の実装例。
package main
import (
"context"
"fmt"
"log"
"net/http"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
"example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)
type GreetServer struct{}
func (s *GreetServer) Greet(
ctx context.Context,
req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
log.Println("Request headers: ", req.Header())
res := connect.NewResponse(&greetv1.GreetResponse{
Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
})
res.Header().Set("Greet-Version", "v1")
return res, nil
}
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
http.ListenAndServe(
"localhost:8080",
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
}
このようなConnectを使用したサーバーに対してcurlのようなHTTP通信もできるし、grpcurlのようなgRPC通信もできるし、protocプラグインであるprotoc-gen-connect-go
を使用して生成したクライアントコードを使って通信もできる。
今までのRESTのようなHTTPベースのAPI開発で使用されるようなHTTP HeaderやInterceptor、エラーハンドリングなどもちゃんと実装方針が示されている。詳しくは以下公式を
Connect for Go
Connect for web
Connectを採用することで今まで通りgRPCを対応しつつ、ConnectやgRPC-webの対応をすることができwebクライアントとのProtobufベースの通信を容易にする。モバイルアプリのサーバーとしてgRPCを採用するケースならば今まで通りgRPCでも問題ないかもしれないが、web側との通信もProtobufを使用したスキーマファーストな開発をしたいというニーズがあれば Connectは採用する価値があるかもしれない。
OpenAPIを利用したクラサバ通信と比較するべき技術かもしれない。
gRPC-web
gRPC公式のチュートリアル
gRPCをwebブラウザ環境で使うためのもの。gRPCをwebブラウザ環境上で使う場合、特別なプロキシサーバーを間に挟んでやり取りする必要がある。これはgRPCがHTTPプロトコル仕様のトレーラを多用しているため。トレーラはHTTPプロトコルの仕様に存在するものの、ほとんど使用されておらずWebブラウザを含む多くのHTTP実装がサポートしていない。そのためgRPCチームはレスポンスボディの最後にトレーラをエンコードするgRPC-webプロトコルを導入した。
connect-go
で構築されたようなgRPCサーバーはgRPC-webプロトコルをネイティブにサポートしているのでブラウザはサーバーと直接通信することができるが、GoogleのgRPC実装のほとんどはgRPC-webをサポートしていないため標準のgRPCプロトコルとの間で変換するためのプロキシサーバーを必要とし、多くの場合Envoy
がプロキシサーバーとして使用される。
ConnectのFAQに詳細あり
CloudflareのgRPC対応のブログになぜgRPC-webが必要なのかが書いてあった
grpc-gateway
gRPCとRESTの両方を対応するためにgRPC-webの他にgrpc-gatewayを使う方法もある。gRPC-webはwebブラウザがgRPCサーバーと通信できるようにプロキシサーバーを立てて通信したが、grpc-gatewayはprotcプラグインを使用してプロキシサーバー用のコードを自動生成しクライアントとgRPCサーバーの間に入る。grpc-gatewayはGoにしか対応していないっぽいがprotoc-gen-openapiv2
などを使用することでOpenAPI対応のREST APIをgRPCと併用してサーバーを構築することなんかもできる。
以下の記事が参考に
grpc-gatewayの嬉しいところがよくまとまっている
gRPCとOpenAPI定義をProtobuf一つで管理できるのともろもろのコードを自動生成できるのがいいとのこと
Envoy
gRPCやProtobufとは直接関係ないけどgRPC-webを利用するのに必要なEnvoy。Envoy自体はマイクロサービスやk8sと同じ文脈で聞くことが多い。nginxと同様OSSのリバプロサーバーを提供するがEnvoyはマイクロサービスのアプリケーションコンテナのサイドカーとして必要な機能を多く含んでいる。
gRPC-webではgRPC非対応のwebブラウザとgRPCサーバー間の通信を可能とするために使用される。
ちなみにマイクロサービスの文脈でEnvoyと一緒によく聞くIstionはマイクロサービス上でEnvoyをいい感じに扱うのに使われるものらしいのでgRPCでIstioは特に関係ない。