Google の protobuf で gRPC Server に REST のエンドポイントをマッピング

Google が公開している [googleapis](https://github.com/googleapis/googleapis)[^1] のソースを使うと REST と gRPC の両プロトコルをサポートできるのでご紹介。

[^1]: もしくは、https://github.com/grpc-ecosystem/grpc-gateway/tree/master/third_party/googleapis

どうしても、HTTP/1.1 で通信したいという局面で知っておくと便利。わたしの場合、ALBで gRPC Server にフォワーディングするとき、ターゲットグループのヘルスチェック用のエンドポイントとして実装した。

そのときは  [envoy](https://www.envoyproxy.io/docs/envoy/latest/) を使って Proxy 立てたのだけれど、今回は [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) を使ってくことにする。

# とりあえず gRPCサーバーの準備

すぐ動くのが欲しかったので [grpc-go の helloworld](https://github.com/grpc/grpc-go) を拝借。

```sh
git clone https://github.com/grpc/grpc-go

# 以降も基本的にこのディレクトリで作業する
cd grpc-go/examples/helloworld/

# gRPCサーバー起動
go run greeter_server/main.go

# クライアントコード実行で動作確認
go run greeter_client/main.go
2020/09/14 23:02:48 Greeting: Hello world
```

.proto ファイルの中身はこんな感じになっている。

```protobuf:grpc-go/examples/helloworld/helloworld.proto
syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
```

# REST API マッピング

### helloworld.proto 修正

`annotations.proto` を import して rpc の定義に `option (google.api.http) {...}` を追記。

```proto:grpc-go/examples/helloworld/helloworld.proto
syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

// 追記
import "google/api/annotations.proto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
      // 追記
      option (google.api.http) = {
        get: "/say_hello"
      };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
```

### googleapis のソースを取ってくる

```sh
go get -u github.com/googleapis/googleapis
```

コンパイル時に参照したいだけなので `git clone` とかでもOK

### 変更した protobuf を元に自動生成

#### コンパイラプラグインインストール

```sh
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/golang/protobuf/protoc-gen-go
```

#### gRPC Server 向け

このとき `google/api/annotations.proto` を参照できるように protoc の引数に`-I`のオプションを追加。 また、既存の `helloworld_grpc.pb.go` と生成されたクライアントコードが重複するので申し訳ないが今回は消してしまう。

```sh
protoc \
  -I. -I$GOPATH/src/github.com/googleapis/googleapis \
  --go_out=plugins=grpc:. --go_opt=paths=source_relative \
  helloworld/helloworld.proto

// 申し訳ないが消す
rm helloworld/helloworld_grpc.pb.go
```

#### Proxy Server 向け

`grpc-gateway` のコンパイラプラグインを使って同様に自動生成。

```sh
protoc -I. -I$GOPATH/src/github.com/googleapis/googleapis \
  --grpc-gateway_out=logtostderr=true,paths=source_relative:. \
  helloworld/helloworld.proto
```

### gRPC Server の実装を修正

生成された `helloworld.pb.go` に合わせてサーバーの実装を修正。

```go:grpc-go/examples/helloworld/greeter_server/main.go
package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	port = ":50051"
)

// 追記
type server struct {}

// 修正: ポインタレシーバに修正
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()

    // 修正: server を代入
	pb.RegisterGreeterServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
```

### Proxy Server 実装

```go:grpc-go/examples/helloworld/greeter_proxy/main.go
package main

import (
	"flag"
	"net/http"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	gw "google.golang.org/grpc/examples/helloworld/helloworld"
)

var (
	tasklistEndpoint = flag.String("tasklist_endpoint", "localhost:50051", "endpoint of YourService")
)

func run() error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *tasklistEndpoint, opts)
	if err != nil {
		return err
	}

	return http.ListenAndServe(":8080", mux)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := run(); err != nil {
		glog.Fatal(err)
	}
}
```

# 動作検証

左:サーバーとプロキシ起動、右:リクエスト
![スクリーンショット 2020-09-15 15.00.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/4c8a9fca-ee2d-9049-ed58-b10314cda9e7.png)

POSTやリクエストヘッダーの設定もできるみたいなので気になる方はどうぞ。
https://grpc-ecosystem.github.io/grpc-gateway/

# 参考

- https://github.com/grpc-ecosystem/grpc-gateway
- https://grpc.io/docs/languages/go/quickstart
- https://github.com/grpc/grpc-go
- https://budougumi0617.github.io/2018/02/03/grpc-gateway-for-rest-api/
- https://qiita.com/ryu3/items/b2882d4f45c7f8485030
- https://grpc-ecosystem.github.io/grpc-gateway/