gRPC、あるいはGraphQL
昨今のAPI開発にgRPC、またはGraphQLを採用するケースは増えてきていて、ドメインロジックをgRPCで実装して、フロントエンド(BFF)はGraphQL経由のHTTP一本で取得するのはベストプラクティスの一つだと思っています。
とはいえ人的リソースや管理の問題から、ProtobufとGraphQLスキーマを両方メンテナンスしていくのはとてもつらくて、 他のエントリでも結果としてどちらかに寄せました、というケースをよく目にしました。
実際自分もやってはみたものの、両方のメンテナンスをしていくのは相当辛くて、Protobufを直してgRPCのI/Fを変更、合わせてGraphQLのSchemaにも手を入れてフロントに渡して…というのは管理コストに見合わないな、ってことで諦めました(今は grpc/grpc-web 、または grpc-ecosystem/grpc-gateway を採用している)。
とはいえGarphQLはリソース群の取得効率が良くて、管理コストを除けば総合的なパフォーマンスを求めて導入したいとは思っていました。GraphiQLなどでフロントからHTTPで気軽に試せるのも良いです。
定義をProtobufに寄せる
何とかできないかな、と試行錯誤する中で、 grpc-gatewayができるなら GraphQLの実行コードもProtobufから自動生成すればいいのでは と思い立って実装したのがこちら。
ysugimoto/grpc-graphql-gateway
protoc のプラグインで、Protobufの各セクションに専用のオプションを定義することでGraphQLのGatewayコードを生成できます。 grpc-gateway のGraphQL版、という感じです。grpc-gatewayほど機能が豊富なわけではないですが、一応CORSなどのmiddlwareの仕組みも入っています。
READMEには書いてないですが、Query以外にもMutationもできます。しかしながら素のGraphQLほど多様な表現はできなくて、Protobufで定義したRPCにGraphQLのQuery/Mutationを対応させて、
- RPCリクエストのmessageを展開してArgument化(Mutationの場合はinput化)する
- RPCレスポンスのmessageを展開、または指定フィールドを抽出してtype化する
- 他、enumや依存している google.protobuf.Timestamp などのパッケージもGraphQL表現に変換
- gRPCサーバへの接続、ResolverでRPCをコールして戻り値に指定、必要ならば接続を閉じる
あたりを自動で実行するコードを生成します。リクエスト毎にgRPCサーバへの接続・切断のパフォーマンスが気になる場合は、自前で接続管理をすることもできます。このあたりもgrpc-gatewayを踏襲しています。
GraphQLのspec実装をイチからやるのは時間がかかるので、GoのOfficial(?)の graphql-go/graphql の実行コードを生成しています。
使い方
README に書いたとおり、今まで通りgRPC用のサービス定義に加えて、GraphQL用のカスタムオプションをいくつか定義して protoc でコンパイルするだけです。
syntax = "proto3";
// プラグイン定義のprotoファイルをimportする(必須)
import "graphql.proto";
service Greeter {
// gRPCサーバの接続設定用 ServiceOptions
option (graphql.service) = {
host: "localhost:50051"
insecure: true
};
rpc SayHello (HelloRequest) returns (HelloReply) {
// GraphQL Query定義用 MethodOptions
option (graphql.schema) = {
type: QUERY // type: QUERYでQuery、MUTATIONでMutationになる
name: "hello" // Query/Mutation名
};
}
}
message HelloRequest {
// GraphQL Arguments設定用 FieldOptions
string name = 1 [(graphql.field) = {required: true}];
}
message HelloReply {
string message = 1;
}
- Resolveする際に接続するgRPCサーバ設定 (ServiceOptions)
- GraphQL Schemaとしてexportする設定 (MethodOptions)
- 引数のrequired / デフォルト値設定 (FieldOptions)
の3つのカスタムオプションを定義しています(実際はもう少し詳細な挙動制御のオプションがある。詳しくは graphql.proto 見てください)。この定義と共にコンパイルすると、xxxx.pb.go
に合わせて xxxx.graphql.go
というファイルが同一パッケージ内に生成されます。
protoc -I. --go_out=plugins=grpc:./ --graphql_out=./ xxxx.proto
実行処理も grpc-gateway
にならって runtime
パッケージを用意していて、これ経由でHTTPサーバを起動すれば、GraphQLのGatewayが手軽に構築できます。
package main
import (
"log"
"net/http"
// 生成したパッケージをimport
"github.com/path/to/generated/code/xxxx"
// HTTPハンドラ用のruntimeをimport
"github.com/ysugimoto/grpc-graphql-gateway/runtime"
)
func main() {
// runtimeを初期化
mux := runtime.NewServeMux()
// GraphQL gateway登録
if err := xxxx.RegisterGreeterGraphql(mux); err != nil {
log.Fatalln(err)
}
// HTTPハンドラとして渡す
http.Handle("/graphql", mux)
log.Fatalln(http.ListenAndServe(":8888", nil))
}
なお、このGatewayはデフォルトで GET/POST 両対応です。OperationNameやVariables付きのリクエストも通ります。
exampleも置いてあるので試してみてください。
単純なGateway、またはBFFとして
Goの定義ファイルが生成されるだけなので、アプリケーションをどう構成するかは自由に決められます。シンプルにgRPCの前段においてGraphQL+gRPCをバンドルする一塊のアプリケーションとしても良いし、BFF的にGraphQLサーバだけ別でどこかに起動し、他に配置したgRPCアプリケーションに流しても良いかと思います。
何よりも定義を共有しているので、 gRPC <-> GraphQL間でI/Fが揃うのが嬉しいですね。
Schemaファイルは生成できないのか
schema.graphql
みたいなファイルの生成も頑張ってますが、色々な制約でまだできてません…。いずれは protoc-gen-graphql-schema
みたいな感じで提供するかも(commitを遡ると途中で消しています。うまく行かなくて一旦諦めた)。
余談
protocプラグインの作り方についてはyuguiさんのエントリを何度も読み返していた。
- https://qiita.com/yugui/items/160737021d25d761b353#protobuf
- https://qiita.com/yugui/items/87d00d77dee159e74886
- https://qiita.com/yugui/items/29adefab34f7f1a3c3c6
この3つを読めばプラグインの作り方は一通りわかります。感謝。プラグイン開発においてはデバッグがしづらく、text/template
からGoのコード生成 -> go/fmt
がうまく行かなかったり割と苦労しました。
またこのプラグインを実装後、 protobuf
本家にグローバルなオプション番号を申請して、発行してもらいました。全世界で一意な番号を貰えるのはわりと嬉しい。
思っていたより少ないので、どんどん作って番号を発行してもらうと良いと思います。 see: https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md
とりあえず自分が使う機能を実装したのと、GraphQLをそこまで深く使いこんでいないので、おそらく知らない仕組みがあったり、機能が足りてなかったりするので、issueやPRを投げてもらえると嬉しいです。
API実装技術選定の一助となれば。