grpc-gateway から connect-go に乗り換えた
概要
タイトルの通り Go で書かれたマイクロサービスを grpc-gateway からconnect-go に書き換えました。
connect-go へのモチベーション
( 個人開発ですが ) ミドルウェアの書き換えと言う一大決心をしたのには以下のようなモチベーションがありました。
Cloud Run の 1 Service 1 Port 制約を乗り越える
API は Google Cloud の Cloud Run で稼働しています。いわゆる Serverless であり非常に簡単にデプロイ出来ることから個人的にも多用させてもらっているのですが、 Cloud Run の 1 つの Service には解放出来る Port が 1 つまでという制約があります。普段はこちらの制約も特には気にならないのですが、 HTTP と gRPC の両方でリクエストを待ち受けたい要件の場合、 1 つの Cloud Run では実現出来ません。そのため個人開発でも HTTP 用と gRPC 用で、同じアプリケーションを 2 つの Cloud Run にデプロイを行なっていました。この構成では単純に使用するクラウドリソースが倍になり、コストにも影響してきます。また、同一のアプリケーションを更新した際に、 2 つの Cloud Run にデプロイする必要があり CI / CD の複雑化の原因にもなっていました。 connect-go では gRPC から生成された Handler を実装するだけで gRPC と HTTP の両方を受け付けるサーバを立てることが出来ます。
立てたサーバに対して gRPC でももちろん、 Content-Type: application/json Header
を付けて送信することで HTTP を受け付けることが出来ます。詳細は GitHub の README をご覧ください。
この仕組みを用いることで Cloud Run でも 1 つの Port で gRPC と HTTP を両方受け付ける構成が取れるようになります。
buf の Remote generation を使ってみたい
上記の制約を乗り越えるだけでも十分に connect-go に乗り換える価値はあったのですが、調べて行くうちに connect で Remote generation と呼ばれる機能があることを知りました。.proto
を書き Buf Schema Registry ( BSR ) にコードを上げることで手元で protoc や buf を使ったコード生成をすることなく gRPC を利用出来る仕組みになっています。
buf はドキュメントが充実しているので詳細の解説は省きますが、 buf registry login
でログインした後 .proto
を記述し、以下のコマンドを打つだけで Module と呼ばれる単位で BSR に Repository を作成することが出来ます。
buf beta registry repository create buf.build/<UserID>/<ModuleName> -visibility private
その後、 buf push --tag v0.0.0
で BSR へコードを push するだけで利用準備は完了します。
利用する側では go.buf.build
からの Import を書くだけで HTTP と gPRC の両方で待ち受けるサーバを構築することが出来ます。
※ --visibility private
で作成した場合、 こちらの手順で GOPRIVATE
と ~/.netrc
の設定が必要になります。
bufbuild/connect-go
の部分は Remote generation の templateOwner, templateName
と呼ばれている Template を選択して記述する形式となっています。
Template を自作することも可能ですが、今回は buf 側で事前に用意されている connect-go の Template を利用するため bufbuild/connect-go
としています。
import (
userv1 "go.buf.build/bufbuild/connect-go/tetsuya28/xxx/user/v1"
)
func main() {
m := http.NewServeMux()
path, handler := notification1.NewNotificationHandler(handler, inter)
m.Handle(path, handler)
http.ListenAndServe(":8080", m)
}
Handler の実装は以下のように Generics を用いた形となっています。 Import のディレクトリは .proto
の package 定義の実装依存ですが、以下の item/v1/item.proto
を以下のように記述している場合はこのような書き方になります。
syntax = "proto3";
package item.v1;
message GetItemRequest {}
message GetItemResponse {}
import (
itemv1 "go.buf.build/bufbuild/connect-go/tetsuya28/xxx/item/v1"
)
type Item interface {
Get(ctx context.Context, in *connect.Request[itemv1.GetItemRequest]) (*connect.Response[itemv1.GetItemResponse], error)
}
また、別のサービスへの呼び出しなどはさらにディレクトリを掘った場所から Import を行います ( こちらも実際のディレクトリは実装に依存するため、適宜変更してください。 ) 。呼び出しも以下のように 1 行書くだけで他のサービスのクライアント生成を行うことが出来ます。
import (
"go.buf.build/bufbuild/connect-go/tetsuya28/xxx/other/v1/otherv1connect"
)
func main() {
otherService := notificationv1connect.NewOtherClient(http.DefaultClient, "http://xxx.xxx")
}
最後に
connect はまだ発展途上であり、メソッドも POST しかサポートされていないなど制約も多いですが、今回乗り換えてみてもかなり記述量も少なく直感的に利用出来るため今後の発展に期待している技術です。
他にも Interceptor や connect-web での repeated
のハマりポイントなどのお話も書こうと思ったのですが力尽きたので気が向いたら書きます。
Discussion