All Articles

Protocol BuffersでgRPCとGraphQL両対応のコードを生成するプラグイン書いた

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を対応させて、

  1. RPCリクエストのmessageを展開してArgument化(Mutationの場合はinput化)する
  2. RPCレスポンスのmessageを展開、または指定フィールドを抽出してtype化する
  3. 他、enumや依存している google.protobuf.Timestamp などのパッケージもGraphQL表現に変換
  4. 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さんのエントリを何度も読み返していた。

この3つを読めばプラグインの作り方は一通りわかります。感謝。プラグイン開発においてはデバッグがしづらく、text/template からGoのコード生成 -> go/fmt がうまく行かなかったり割と苦労しました。

またこのプラグインを実装後、 protobuf 本家にグローバルなオプション番号を申請して、発行してもらいました。全世界で一意な番号を貰えるのはわりと嬉しい。 思っていたより少ないので、どんどん作って番号を発行してもらうと良いと思います。 see: https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md

とりあえず自分が使う機能を実装したのと、GraphQLをそこまで深く使いこんでいないので、おそらく知らない仕組みがあったり、機能が足りてなかったりするので、issueやPRを投げてもらえると嬉しいです。

API実装技術選定の一助となれば。