Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

gRPC-Gateway v2へのアップグレードで対応したこと

サーバーサイドエンジニアの田実です!

YappliではネイティブアプリのAPIでgRPC-Gatewayを使って実装しています。 今回は、gRPC-Gatewayをv1からv2にアップグレードしたときに対応したことを紹介します!

マイグレーションガイドはこちら↓ github.com

1. Goのパッケージ名を変更

- "github.com/grpc-ecosystem/grpc-gateway/runtime"
+ "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"

2. MarshalarOptionを変更

+ "google.golang.org/protobuf/encoding/protojson"

- runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: true, EmitDefaults: true}),
+ runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
+   MarshalOptions: protojson.MarshalOptions{
+       UseProtoNames:   true,
+       EmitUnpopulated: true,
+   },
+   UnmarshalOptions: protojson.UnmarshalOptions{
+       DiscardUnknown: true,
+   },
+ }),

UseProtoNames はフィールド名にprotoファイルの名前をそのまま使うか(true)、lowerCamelを使うか(false)を制御するパラメータです。 v1ではruntime.JSONPbのOrigNameフィールドに該当します。

EmitUnpopulated はゼロ値をJSONの値で返すか(true)、省略するか(false)を制御するパラメータです。 v1ではruntime.JSONPbのEmitDefaultsフィールドに該当します。

DiscardUnknown は定義されていないフィールドが送られたときの挙動を制御するパタメータで、trueの場合は無視します。 v1では runtime. DisallowUnknownFields() に相当します。

例ではruntime.JSONPbを使っていますが、これをラップしたruntime.HTTPBodyMarshalerを使ってもOKです。

&runtime.HTTPBodyMarshaler{
    Marshaler: &runtime.JSONPb{
        // ...
    }
}

HTTPBodyMarshalerはハンドラー側がapi.HttpBodyの型で返した場合、HttpBody.Dataがそのままレスポンスで返されます。これによってJSON以外やprotoファイルに依存しないレスポンスを返すことができます。

// Marshal marshals "v" by returning the body bytes if v is a
// google.api.HttpBody message, otherwise it falls back to the default Marshaler.
func (h *HTTPBodyMarshaler) Marshal(v interface{}) ([]byte, error) {
    if httpBody, ok := v.(*httpbody.HttpBody); ok {
        return httpBody.Data, nil
    }
    return h.Marshaler.Marshal(v)
}

grpc-gateway/marshal_httpbodyproto.go at 24434e22fb9734f1a62c81c4ea246125d7844645 · grpc-ecosystem/grpc-gateway · GitHub

github.com

3. マッチルール変更の対応

あるURLに対してマッチするパターンが2つ以上ある場合、最後にマッチしたものが優先されます(WithLastMatchWins)。 v1ではデフォルトで最初にマッチしたものが優先されるので、もしこのパターンが存在する場合はprotoファイルを変更する必要があります。

例えば以下のようなprotoがあったときに、 /hoge/fuga にリクエストするとv1では GetHogeFuga が呼び出され、v2では GetHoge でidパラメータに fuga が入った状態で呼び出されます。

service HogeService {
  rpc GetHogeFuga(GetHogeFugaRequest) returns (GetHogeFugaResponse) {
    option (google.api.http) = {
      get: "/hoge/fuga"
    };
  }
  rpc GetHoge(GetHogeRequest) returns (GetHogeResponse) {
    option (google.api.http) = {
      get: "/hoge/{id}"
    };
  }
}

この場合、アップグレード時にrpcの定義順を並び替えるとアップグレード前と同じ挙動になります。

4. protoファイルでSwaggerのannotationを変更

Swaggerのannotationを使っている場合は以下のように名前を変更します。

- import "protoc-gen-swagger/options/annotations.proto";
+ import "protoc-gen-openapiv2/options/annotations.proto";

- option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {

5. protoc実行オプションなどの修正

protocプラグインもv2にします。

- go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
+ go get -u "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

protocで --swagger_out になっているところを --openapiv2_out に変更します。

- protoc --swagger_out=...
+ protoc --openapiv2_out=...

また、googleapisは v1のgrpc-gateway/third_party 内にあったのですが
v2だと https://github.com/googleapis/googleapis リポジトリにあるので、こちらも対応が必要です。

まとめ

gRPC-Gatewayのv1からv2にアップグレードしたときに対応したことを紹介しました!

特にマッチルール変更はE2Eレベルのテストが無いと気付きづらいので注意が必要です 💦