[gRPC-Gateway, Envoy]  gRPCとRESTを繋げたい時に．．．


## 前書き

今回の記事では[gRPC](https://www.grpc.io/docs/what-is-grpc/introduction/)についての基礎的な説明と[Protocol Buffers](https://developers.google.com/protocol-buffers)の記述方法にはあまり触れません。以前に[Go言語+gRPCをはじめから丁寧に解説してみた[ハンズオン]](https://qiita.com/ishishow/items/c9b633d3d4d22db74a6d)という記事を作成したのでそちらを踏まえた上で本記事を閲覧してもらえると助かります。



## この記事の内容

今回はGolangにおいてgRPCサーバにREST形式でアクセスしたい時のアプローチについて説明します。

この記事を読むことにより、

-  [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway)を使ってアクセスする方法
-  [Envoy](https://www.envoyproxy.io/docs/envoy/latest/)の[gRPC-JSON-transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)フィルタを使ってアクセスする方法

以上二つのアプローチの概観を理解することができます。



早速取り掛かりましょう！



## [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway#grpc-gateway)

#### アーキテクチャ図

![img](https://github.com/grpc-ecosystem/grpc-gateway/raw/master/docs/assets/images/architecture_introduction_diagram.svg)

[#公式より抜粋](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/docs/assets/images/architecture_introduction_diagram.svg)



上図を見ると、アクセスしたいgRPCサービスにリバースプロキシ的な役割を持つサーバーをgrpc-gatewayが作成することでAPIクライアントからREST形式で繋げるようにしています。

また、あくまで左側のprotoファイルから[スタブ](https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/generating_stubs/using_protoc/)を生成していることがわかります。



## 実装

今回は私が作成した[前回の記事](https://qiita.com/ishishow/items/c9b633d3d4d22db74a6d)で使用したサンプルリポジトリをベースに変更していきます。

サンプルリポジトリ　⬇︎

https://github.com/ishishow/grpc_tutorial



また、完成系は別途リポジトリを分けて作成しており、以下のリポジトリの[grpc-gatewayブランチ](https://github.com/ishishow/grpc_tutorial2)を参考にしていただけると幸いです。

https://github.com/ishishow/grpc_tutorial2



#### フォルダ構成

今回のフォルダ構成は以下になっています。

```shell
.
└── grpc_tutorial
    ├── Makefile
    ├── chat
    │   ├── chat.go
    │   ├── chat.pb.go
    │   ├── chat.pb.gw.go
    │   └── chat_grpc.pb.go
    ├── chat.proto
    ├── cmd
    │   ├── gateway
    │   │   └── main.go
    │   └── server
    │       └── main.go
    ├── go.mod
    └── go.sum

5 directories, 10 files
```





#### プラグインのインストール

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



今回インストールはこれで終了です。



#### Makefileの作成

protocコマンドをが長くて見にくいので筆者はよくMakefileにコマンドを貼り付けています。

```makefile
protoc:
	protoc -I/usr/local/include -I.\
  -I/usr/local/opt/protobuf/include \
  -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis 
  --plugin=protoc-gen-grpc-gateway=$(GOPATH)/bin/protoc-gen-grpc-gateway --grpc-gateway_out=logtostderr=true:./chat ./chat.proto \
	&& protoc -I/usr/local/include -I. \
	-I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
	-I/usr/local/opt/protobuf/include \
	--go_out=./chat --go-grpc_out=./chat ./chat.proto

```

こちら筆者が行った時はv1.16.0だったのですが、違う場合があります。その場合は自分のバージョンを調べて適宜修正ください。(恐らくmodule not found が出ると思われます。)

#### コマンドの実行

```shell
$ make protoc
```

こちらのコマンドでchatディレクトリ内にchat_grpc.pb.go, chat_pb.go, chat.pb.gw.goが生成されたかと思います。もしエラーが発生するようでしたら、フォルダ構成の見直し、gopathの見直しを行ってください。





#### chat.protoの編集

 chat.proto

```protobuf
syntax = "proto3";
package chat;

import "google/api/annotations.proto";

service ChatService {
  rpc SayHello(MessageRequest) returns (MessageResponse) {
    option (google.api.http) = {
      get: "/hello"
    };
  }
}

message MessageRequest {
  string body = 1;
}

message MessageResponse {
  string body = 1;
}

```

こちらはgatewayとは関係ないですが、一般的にリクエストとレスポンスの型を作成することが多いため、変更しました。



```protobuf
import "google/api/annotations.proto";

option (google.api.http) = {
      get: "/hello"
 };
```

gatewayに関わるコードは上記です。詳細の動きを追いたい方は[こちら](https://github.com/googleapis/googleapis/blob/master/google/api/annotations.proto)をご覧ください



#### gatewayサーバーの実装

 gateway/main.go

```go
package main

import (
	"flag"
	"fmt"
	"net/http"

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

	"grpc-intro/chat"
)

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

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	endpoint := fmt.Sprintf("localhost:9000")
	err := chat.RegisterChatServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)
	if err != nil {
		return err
	}

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

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

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

```

5000ポートでAPIリクエストを受け取り、9000ポートのgrpcサーバーに渡す処理を書いています。

```go
	err := chat.RegisterChatServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)
```

この部分が定義したサービスによって変わってきます。



実装完了です！

試してみます。

```shell
go run cmd/server/main.go
//Go gRPC Beginners Tutorial!

// 別ターミナル
go run cmd/gateway/main.go

//別ターミナル
curl localhost:5000/hello
// {"body":"Hello From the Server!"}
```



動作確認成功です！

これにてgrpc-gatewayの基本的な使い方は完了です！



## [Envoy(gRPC-JSON-transcoder)](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)

![ITOps Times Open-Source Project of the Week: Envoy - ITOps Times](https://www.itopstimes.com/wp-content/uploads/2019/10/Envoy_Logo_Final_PANTONE.png)



さて、二つ目のアプローチです。こちらは[Envoy](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)の[gRPC-JSON-transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)というHTTPフィルターを使用します。

↓完成形です。

https://github.com/ishishow/grpc_tutorial2/tree/envoy_grpc_json_transcorder



先ほどのgrpc-gatewayのブランチと比較した変更点は[こちら](https://github.com/ishishow/grpc_tutorial2/compare/envoy_grpc_json_transcorder)になっています。



フォルダ構造を表示します。

```shell
.
├── Makefile
├── chat
│   ├── chat.go
│   ├── chat.pb.go
│   └── chat_grpc.pb.go
├── chat.proto
├── cmd
│   └── server
│       └── main.go
├── docker-compose.yaml
├── envoy
│   ├── envoy.yaml
│   └── proto.pb
├── go.mod
└── go.sum

4 directories, 11 files
```



大きく異なる点はenvoyフォルダがあることと、gatewayフォルダがなくなっていることですね。

それでは変更ファイルをみていきます。



#### Makefile

Makefile

```makefile
envoy_protoc:
	protoc -I/usr/local/include -I. -I/usr/local/opt/protobuf/include -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
	--include_imports \
	--include_source_info \
	--descriptor_set_out=./envoy/proto.pb \
  	./chat.proto
```

envoyが読み取るためのproto.pbバイナリを作成してenvoyフォルダに設置します。

バージョンが違う恐れがあるので適宜修正ください。



```shell
$ make envoy_protoc
```

ここでエラーが発生したらバージョン、gopathを確認お願いします。



#### docker-compose.yaml

 docker-compose.yaml

```yaml
version: "3"

services:
  envoy:
    image: envoyproxy/envoy:v1.13.1
    volumes:
      - "./envoy:/etc/envoy"
    expose:
      - 51051
    ports:
      - 51051:51051
```

envoyのイメージを使います。51051ポートがデフォルトのようなので今回はそのままにしました。

volumesでenvoyフォルダをマウントしています。



#### envoy.yaml

 envoy/envoy.yaml

```yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
  listeners:
    - name: listener1
      address:
        socket_address: { address: 0.0.0.0, port_value: 51051 }
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
                stat_prefix: grpc_json
                codec_type: AUTO
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        # NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path.
                        # Reference: https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
                        - match: { prefix: "/" }
                          route: { cluster: grpc, timeout: { seconds: 60 } }
                http_filters:
                  - name: envoy.grpc_json_transcoder
                    config:
                      proto_descriptor: "/etc/envoy/proto.pb"
                      services: ["chat.ChatService"]
                      print_options:
                        add_whitespace: true
                        always_print_primitive_fields: true
                        always_print_enums_as_ints: false
                        preserve_proto_field_names: false
                  - name: envoy.router

  clusters:
    - name: grpc
      connect_timeout: 1.25s
      type: logical_dns
      lb_policy: round_robin
      dns_lookup_family: V4_ONLY
      http2_protocol_options: {}
      load_assignment:
        cluster_name: grpc
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # WARNING: "docker.for.mac.localhost" has been deprecated from Docker v18.03.0.
                      # If you're running an older version of Docker, please use "docker.for.mac.localhost" instead.
                      # Reference: https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18030-ce-mac59-2018-03-26
                      address: 192.168.xxx.xxx
                      port_value: 50051

```

この設定は[公式](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#sample-envoy-configuration)の設定からほぼ変えていません。

```yaml
     proto_descriptor: "/etc/envoy/proto.pb"
     services: ["chat.ChatService"]            
```

こちらの部分と、アドレス部分を変更しました。

```yaml
    address: 192.168.xxx.xxx
    port_value: 50051
```

addressは今回goをローカル環境で動かしているため、自分のpcのアドレスを指定してあげる必要があります。

ipconfigなどのコマンドを打てばわかるので各自確認して追加してください。



#### サーバーポートの変更

envoyの設定を変えればいいのですが、server.goを一行編集すればいいかと思い、ここは楽をしてしまいました。

```go
package main

import (
	"fmt"
	"log"
	"net"

	"grpc-intro/chat"

	"google.golang.org/grpc"
)

func main() {

	fmt.Println("Go gRPC Beginners Tutorial!")

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 51051)) // ここだけ変える
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := chat.Server{}

	grpcServer := grpc.NewServer()

	chat.RegisterChatServiceServer(grpcServer, &s)

	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %s", err)
	}
}

```

これでポートが変わりました。





### 実行

```shell
$ docker-compose up -d
$ go run cmd/server/main.go

// ターミナル変える
$ curl localhost:51051/hello
// {"body":"Hello From the Server!"}
```



完成です！



## 最後に

ここまででgRPCとRESTをつなげる方法を二つ紹介しました。grpc-gatewayはgolangのみのサポートですが,envoyを使った方法は異なる言語でも活用できるので柔軟です。
[ZOZOの導入事例の記事](https://techblog.zozo.com/entry/zozomat-envoy-grpc-json-transcoder)を引用すると
> grpc-gatewayがあります。こちらもHTTP JSON APIリクエストをgRPCに変換して背後にあるgRPCサーバーへプロキシするものです。以下の図にgrpc-gatewayを利用する構成の例を示します。gRPCとHTTP JSON APIでリクエストの経路を分離する場合やEnvoyではないコンポーネントを使う場合は良いかもしれません。一方ZOZOMATシステムの場合、両者とも同じ経路となるためEnvoyを利用、かつgRPC-JSON transcoderを有効にした構成となっています。

と書かれています。Envoyを使っている場合はそのままフィルタ機能を使った方が良さそうです。



もしもこの記事に不明瞭な点がありましたら気軽にコメントをお願いします。



ここまで読んでくださりありがとうございました！



### 参考文献

https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter

https://github.com/grpc-ecosystem/grpc-gateway

https://qiita.com/nisitanisubaru/items/abe11ff4248997d1fee2

https://techblog.zozo.com/entry/zozomat-envoy-grpc-json-transcoder