こんにちは、ティアフォーで認証認可基盤を開発している澤田です。
最近取り入れたProtobufで、素晴らしいREST APIの開発体験をしたのでご紹介します。
なお、ティアフォーではマイクロサービスを支える認証認可基盤を一緒に開発いただけるメンバーを募集しています。ご興味のある方は下記ページからご応募ください。
実現したかったこと
マイクロサービス間連携のAPI開発において、以下の条件を満たすやり方を探していました。
- スキーマを最初に定義してリクエストとレスポンスの型が自動で生成される
- ドキュメント(openapi.yaml)が生成される
- バリデーションが定義できて、その実装が自動で生成される
実現方法
Go言語で開発する場合はgo-swaggerでも実現できますが、本記事では、Protobufで実現できるgRPC Gatewayとprotoc-gen-validate (PGV)を使った方法をご紹介します。
gRPC Gateway
https://github.com/grpc-ecosystem/grpc-gateway
gRPC GatewayはProtobufの定義からREST APIのプロキシを生成してくれるプラグインです。gRPCサーバの前段に置くことで、REST APIのインターフェイスを提供することができます。
単にREST APIを開発する場合は、gRPCサーバは不要なので、gRPC Gatewayにサービスの実装を登録することができます。
以下の例のように、Protobufから生成されたRegisterXXXHandlerServer
を利用すれば、Gatewayにサービスの実装を登録することができます。
mux := runtime.NewServeMux() err := pb.RegisterEchoServiceHandlerServer(ctx, mux, &EchoHandler{}) if err != nil { // Error Handling }
ご参考までにProtobufの定義も載せておきます。message定義の中で必須のフィールドを指定しておくと、swaggerとして吐き出されるときにrequiredとして表現されます。設定を忘れやすいところではありますが、Goの場合は生成したopenapi.yamlからAPI Clientを自動生成し、インテグレーションテストの仕様により必須でないフィールドはポインタになるため、設定を忘れていたことに気づけます。
syntax = "proto3"; package example; option go_package = "./;pb"; import "google/api/annotations.Protobuf"; import "validate/validate.Protobuf"; import "Protobufc-gen-openapiv2/options/annotations.Protobuf"; service EchoService { rpc Echo (EchoRequest) returns (EchoResponse) { option (google.api.http) = { post: "/echo" body: "*" }; } } message EchoRequest { string name = 1; option (grpc.gateway.Protobufc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { required: ["name"] } }; } message EchoResponse { string message = 1; option (grpc.gateway.Protobufc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { required: ["message"] } }; }
ちなみに、生成されるドキュメントはOpenAPI v2なので、v3の形式で欲しい場合は以下のようなツールを使ってさらに変換をかける必要があります。
https://github.com/Mermade/oas-kit/blob/main/packages/swagger2openapi/README.md
また、openapi.yamlからAPI Clientの自動生成はoapi-codegenを利用しています。
https://github.com/deepmap/oapi-codegen
protoc-gen-validate (PGV)
https://github.com/envoyproxy/Protobufc-gen-validate
ProtobufからgRPCのメッセージバリデーションを生成してくれるプラグインです。アノテーションでバリデーションルールを表現できます。
syntax = "proto3"; package examplepb; import "validate/validate.Protobuf"; message Person { uint64 id = 1 [(validate.rules).uint64.gt = 999]; string email = 2 [(validate.rules).string.email = true]; string name = 3 [(validate.rules).string = { pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$", max_bytes: 256, }]; Location home = 4 [(validate.rules).message.required = true]; message Location { double lat = 1 [(validate.rules).double = { gte: -90, lte: 90 }]; double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }]; } }
gRPCサーバがある場合は、Interceptorでバリデーションできますが、サービスを登録したgRPC Gatewayで使う場合はInterceptorの出番がなく、またHTTPのMiddlewareレイヤではバリデータが使用できません。そのため、Handler内で request.Validate()
メソッドを呼ぶ必要があります。
愚直にHandlerの処理でバリデーションメソッドを呼んでもいいのですが、私のチームでは、専用のレイヤでバリデーションするようにしました。コード量は増えてしまいますが、バリデーションの実装忘れは発生しにくいと思います。
BufでProtobufをビルドする
REST APIに限った話ではないのでテーマから少し脱線してしまいますが、開発体験を良くしてくれたBufについても簡単にご紹介させてください。
BufはProtobufの依存管理やprotocで実行していたコマンドをいい感じにまとめてくれるツールです。これを使うことで、 依存管理の悩みから開放され、protocコマンドで長たらしく書いていたものをbuf generate
とシンプルにまとめることができます。
https://docs.buf.build/introduction
protoディレクトリをprotoファイルの置き場とした場合、以下のような構成になります。
RepositoryRoot ├── buf.gen.yaml // protoc コマンドの引数ここで定義する ├── buf.work.yaml // workspaceを指定。この場合は proto ディレクトリを指定する └── proto ├── buf.lock // buf.yaml を作成後、 buf mod update コマンドで自動生成される ├── buf.yaml // .proto ファイルで使用されている依存の定義 └── example └── echo.proto
BSR(Buf Schema Registry)
BufはBSRというProtobuf(Repository)とプラグインのレジストリを提供しています。
Protobufを提供しているRepositoryは充実していますが、プラグインはBSRに登録されていないことがあります。例えば、protoc-gen-validateのRepositoryはありますが、2022年1月現在プラグインは提供されていないので、プラグインが実行可能なDockerfileを自分で用意、BSRに登録し、使えるようにする必要があります。
まとめ
今回は、Protobufを使ってREST APIを開発する方法と、ProtobufのツールであるBufについてご紹介しました。バリデーションまでできるスキーマ駆動開発の方法はかなり少ないと感じており、今後しばらくは有力な選択肢の一つになるのではないかと思っています。