Better gRPC な Connect に乗り換える - Go言語編

Better gRPC な Connect に乗り換える - Go言語編

デジタル認知行動療法アプリ Awarefy は、2022年4月からバックエンドシステムを Go + gRPC / Protocol Buffers を用いて開発・運用しています。現在進行中の Web アプリ開発のために、connect-go への切り替えが事実上必要になったため、grpc-go から connect-go へのマイグレーションを実行しました。

Connect とは

そもそも Connect とはなにかですが、Better gRPC と理解するのがよいでしょう。

Getting started | Connect
Connect is a slim library for building browser- and gRPC-compatible HTTP APIs.
Connect is a slim library for building browser- and gRPC-compatible HTTP APIs.

Connect の開発元は gRPC に並々ならぬ情熱を注ぐ Buf という組織です。

Buf
Create, maintain and consume Protocol Buffers APIs with our modern Protocol Buffers ecosystem

Buf は、Connect 以前から Protocol Buffers  の スキーマファイルのビルドツール や、スキーマのレジストリ を提供していました。

既存の Specification が微妙なので(筆書意訳)、より厳格で分かりやすいものを作りました、というあたりからも実行力の高さが窺えます。

The Protobuf Language Specification
A comprehensive definition of the language, to empower a vibrant Protobuf ecosystem.

そんな Buf が Connect の構想を表明したのは 2022年1月のことでした。

Connect: A better gRPC
Use Connect to build simple, stable, browser and gRPC-compatible APIs.

ということで、すでに 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 に比べてデバッグが行いやすい

詳細は公式ドキュメントに記載があるので参照ください。

Introduction | Connect
Connect is a family of libraries for building browser and gRPC-compatible HTTP

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 に対応した クライアント/サーバー アプリを開発するためのライブラリです。

GitHub - bufbuild/connect-go: Simple, reliable, interoperable. A better gRPC.
Simple, reliable, interoperable. A better gRPC. Contribute to bufbuild/connect-go development by creating an account on GitHub.

Go は元々 Web Application Framework を利用することなく Web アプリが開発できることで知られています。connect-go はサードパーティのツールですが、標準の http ライブラリに乗る形になることと、依存するところというとリクエスト/レスポンスのデータ型を connect-go が提供するものに変更する程度であるため、Go の思想を汲んだ仕上がりになっていると思います。

Connect 登場以前、Go 言語で gRPC の Webサーバーを開発するには grpc-go が事実上必須でした。

GitHub - grpc/grpc-go: The Go language implementation of gRPC. HTTP/2 based RPC
The Go language implementation of gRPC. HTTP/2 based RPC - GitHub - grpc/grpc-go: The Go language implementation of gRPC. HTTP/2 based RPC

grpc-go は grpc-go のお作法に従う必要があるため、RESTFul API 開発の知識からの差分がいくつかありました。Connect のほうがその差分が少ないと言えるでしょう。

connect-go  を使ったデモアプリが公開されています。

GitHub - bufbuild/connect-demo: An example service built with Connect.
An example service built with Connect. Contribute to bufbuild/connect-demo development by creating an account on GitHub.

実装の全体感を掴むのによいでしょう。

Connect を使った開発の流れ

Connect を使った開発の流れは次のようになります。

  1. Protocol Buffers の定義書を書く
  2. Buf コマンドでコード生成する
  3. バックエンドの実装をする

実際この開発フローは 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 を紹介します。コードは断片しか掲載しないので、上述のデモアプリなどと比較して読み進めてください。

公式からもマイグレーションガイドが提供されています。

gRPC compatibility | Connect
Connect fully supports the gRPC and gRPC-Web protocols, including streaming. We

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(),
	),
))
Interceptors | Connect
Interceptors are similar to the middleware or decorators you may be familiar

検証できていること / できていないこと

検証できていること

  • grpc-go から  connect-go への乗り換えが行えること
  • バックエンドは connect-go かつ、クライアントは grpc-dart の組み合わせで動作すること
  • 上記構成が Amazon Web Service (AWS) の Application Load Balancer を介しても動作すること

検証できていないこと

  • grpc-web との組み合わせ(開発チームが検証中のため、検証結果が公表されるはず!)

おそらく今後、gRPC 対応の アプリを開発する場合、Connect がデファクトスタンダードになっていくのではないかと予想します。

以上です。