Better gRPC な Connect に乗り換える - Go言語編
デジタル認知行動療法アプリ Awarefy は、2022年4月からバックエンドシステムを Go + gRPC / Protocol Buffers を用いて開発・運用しています。現在進行中の Web アプリ開発のために、connect-go への切り替えが事実上必要になったため、grpc-go から connect-go へのマイグレーションを実行しました。
Connect とは
そもそも Connect とはなにかですが、Better gRPC と理解するのがよいでしょう。
Connect is a slim library for building browser- and gRPC-compatible HTTP APIs.
Connect の開発元は gRPC に並々ならぬ情熱を注ぐ Buf という組織です。
Buf は、Connect 以前から Protocol Buffers の スキーマファイルのビルドツール や、スキーマのレジストリ を提供していました。
既存の Specification が微妙なので(筆書意訳)、より厳格で分かりやすいものを作りました、というあたりからも実行力の高さが窺えます。
そんな Buf が Connect の構想を表明したのは 2022年1月のことでした。
ということで、すでに gRPC / Protocol Buffers を用いた開発体験の中心に存在する Buf の直近の動きが Connect です。
あらすじここまで。
Connect にすると何が良いのか
Connect にするメリットは以下のようなものがあげられます。
- 1つのコードで gRPC, Connect, grpc-web の3つのプロトコルをサポートできる
- ブラウザ上の JavaScript をクライアントとする場合の gRPC の問題を解消している(grpc-web)
- gRPC 互換のため、クライアントコードは Connect に非対応でも動作する
- 伝統的な RESTFul API / JSON (
application/json
) のリクエストも受け付けるため(ただしPOSTのみ)、gRPC に比べてデバッグが行いやすい
詳細は公式ドキュメントに記載があるので参照ください。
Connect の各言語のサポート状況
2022年3月22日時点、Buf がサポートしている(= コード生成ツールが存在する)のは、Go、Kotlin、Swift、Web、Node です。
これらはあくまで Connect のサポートであり、その他の言語をクライアントとする場合、Connect ではなく 後方互換性によって動作する gRPC で処理を行います。
Awarefy のアプリは Flutter で開発しているため、Dart の対応が待たれます。非Connect な gRPC であれば、Flutter / Dart 側のコードは変更しなくて済むのはよいところです。
connect-go
connect-go は、Go言語で Connect に対応した クライアント/サーバー アプリを開発するためのライブラリです。
Go は元々 Web Application Framework を利用することなく Web アプリが開発できることで知られています。connect-go はサードパーティのツールですが、標準の http
ライブラリに乗る形になることと、依存するところというとリクエスト/レスポンスのデータ型を connect-go が提供するものに変更する程度であるため、Go の思想を汲んだ仕上がりになっていると思います。
Connect 登場以前、Go 言語で gRPC の Webサーバーを開発するには grpc-go が事実上必須でした。
grpc-go は grpc-go のお作法に従う必要があるため、RESTFul API 開発の知識からの差分がいくつかありました。Connect のほうがその差分が少ないと言えるでしょう。
connect-go を使ったデモアプリが公開されています。
実装の全体感を掴むのによいでしょう。
Connect を使った開発の流れ
Connect を使った開発の流れは次のようになります。
- Protocol Buffers の定義書を書く
- Buf コマンドでコード生成する
- バックエンドの実装をする
実際この開発フローは gRPC の場合と変わりません。
buf.gen.yaml
の記述例は以下のとおりです。
version: v1
managed:
enabled: true
plugins:
- name: go
out: gen
opt: paths=source_relative
- name: connect-go
out: gen
opt: paths=source_relative
grpc-go から connect-go に移行するために
grpc-go から connect-go にマイグレーションするための TIPS を紹介します。コードは断片しか掲載しないので、上述のデモアプリなどと比較して読み進めてください。
公式からもマイグレーションガイドが提供されています。
HTTP サーバー
ある意味一番の差分かも知れません。
s := grpc.NewServer()
grpc-go に依存していた部分がまるごと不要になり http.Server
を利用したコードに変更します。
mux := http.NewServeMux()
srv := &http.Server{
Addr: fmt.Sprintf(":%v", port),
Handler: h2c.NewHandler(
mux,
&http2.Server{},
),
}
このあたりはむしろ Connect にすることで、標準の Web アプリ開発に近くなる点かと思います。
リクエスト / レスポンス
リクエストおよびレスポンスの処理については、リクエストを *connect.Request
で、レスポンスを *connect.Response
で ラップするのみです。一括置換でも対応できる程度の変更です。
type healthCheckController struct{}
func NewHealthCheckServiceServer() svc.HealthCheckServiceHandler {
return &healthCheckController{}
}
func (h healthCheckController) Check(
context.Context,
*connect.Request[pb.HealthCheckRequest],
) (
*connect.Response[pb.HealthCheckResponse],
error,
) {
return connect.NewResponse(&pb.HealthCheckResponse{}), nil
}
※ svc.
および pb.
で始まるコードは buf コマンドにより自動生成されたファイルをインポートして利用している箇所です。
エラーコード
繰り返しになりますが gRPC 互換なので、gRPC 向けにはエラーを表現するライブラリ google.golang.org/grpc/status
をそのまま使うこともできます。
Connect がエラー関連機能を提供しているので、こちらに切り替えるのがベターでしょう。ほぼ、機械的に変更が可能です。
connect.NewError(connect.CodeUnauthenticated, errors.New("failed to get a token"))
インターセプター(ミドルウェア)
インターセプター(ミドルウェア)についても変更差分が大きい箇所の1つです。
以下はロギングを行うインターセプターの例です。
func NewLoggingInterceptor() connect.UnaryInterceptorFunc {
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return connect.UnaryFunc(func(
ctx context.Context,
req connect.AnyRequest,
) (connect.AnyResponse, error) {
Logger.Info(
"Request",
zap.String("Procedure", req.Spec().Procedure),
zap.String("Protocol", req.Peer().Protocol),
zap.String("Addr", req.Peer().Addr),
)
return next(ctx, req)
})
}
return connect.UnaryInterceptorFunc(interceptor)
}
next()
の上に書いたコードがリクエスト時にとおる処理、したに書いたコードがレスポンス時にとおる処理です。
インターセプターは登録して利用します。
mux := http.NewServeMux()
mux.Handle(cg.NewHealthCheckServiceHandler(
controller.NewHealthCheckServiceServer(),
connect.WithInterceptors(
interceptor.NewLoggingInterceptor(),
),
))
検証できていること / できていないこと
検証できていること
grpc-go
からconnect-go
への乗り換えが行えること- バックエンドは
connect-go
かつ、クライアントはgrpc-dart
の組み合わせで動作すること - 上記構成が Amazon Web Service (AWS) の Application Load Balancer を介しても動作すること
検証できていないこと
grpc-web
との組み合わせ(開発チームが検証中のため、検証結果が公表されるはず!)
おそらく今後、gRPC 対応の アプリを開発する場合、Connect がデファクトスタンダードになっていくのではないかと予想します。
以上です。