gRPC-WebのProxyをNginxにしてみた

先日gRPC-WebがGAされました。
[gRPC-Webが正式リリース。WebブラウザからgRPCを直接呼び出し可能に](https://www.publickey1.jp/blog/18/grpc-webwebgrpc.html)

どのような仕組みになっているかというと・・・

```
Client(ブラウザ) -> Proxy -> gRPCサーバー
```

というふうにProxyを介してブラウザとgRPCサーバーとでやり取りを行っています。
今回はこの**Proxy**のお話です。

このProxyは、公式ドキュメントやその例などでも**Envoy**を使用しています。
https://github.com/grpc/grpc-web

実際、Envoyを利用するとすんなりと導入することが出来ます。
ただIP制限ができなかったり(こちら出来るようであればご指摘おねがいします）、Yamlで書かないといけなかったりとなにかとかゆいところに手が届きません。
そのため、ProxyをNginxにしてやってみようと思います。

NginxでもgRPCのプロキシーをすることができます。
[Introducing gRPC Support with NGINX 1.13.10](https://www.nginx.com/blog/nginx-1-13-10-grpc/)

今回作るサンプルはこちらに配置しています。
https://github.com/morix1500/grpc-web-nginx

## protoファイルの作成
まずはprotoファイルを作成しインターフェイスを定義します。
名前が渡されたら、「Hello, ○○」と返却するだけのAPIです。

```proto:proto/hello.proto
syntax = "proto3";

package hello;

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

service HelloService {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}
```

ここからクライアントとサーバーのコードを生成します。
今回は便利な [znly/protoc](https://github.com/znly/docker-protobuf) を使用します。

```bash
$ docker run --rm -v $(pwd):$(pwd) \
-w $(pwd) znly/protoc:0.4.0 \
-I ./proto \
--js_out=import_style=commonjs,binary:./client/assets/_proto \
--go_out=plugins=grpc:./server/proto/ \
--grpc-web_out=import_style=commonjs,mode=grpcweb:./client/assets/_proto \
proto/hello.proto 

$ tree .
.
├── README.md
├── client
│   └── assets
│       └── _proto
│           ├── hello_grpc_web_pb.js
│           └── hello_pb.js
├── proto
│   └── hello.proto
└── server
    └── proto
        └── hello.pb.go
```

## gRPCサーバーの作成
サクッと作りましょう。
Golangで作ります。`main.go` は省略します。

```go:server/grpc/server.go
package grpc

import (
        "context"
        "fmt"
        pb "github.com/morix1500/grpc-web-nginx/server/proto"
        "google.golang.org/grpc"
        "net"
)

type HelloService struct{}

func (h HelloService) Run() int {
        s := grpc.NewServer()
        pb.RegisterHelloServiceServer(s, h)

        lis, err := net.Listen("tcp", ":5000")
        if err != nil {
                fmt.Printf("%+v\n", err)
                return 1
        }
        if err := s.Serve(lis); err != nil {
                fmt.Printf("%+v\n", err)
                return 1
        }

        return 0
}

func (h HelloService) Hello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
        return &pb.HelloResponse{
                Message: "Hello, " + in.Name,
        }, nil
}
```

以下のコマンドで起動できます。

```bash
$ cd server/
$ go run main.go
```

## gRPCクライアントの作成
クライアントはブラウザなので、JavaScriptです。
今回はNuxt.jsでサクッと作ってみます。

```vue:client/pages/index.vue
<template>
  <div>
    <h1>Test</h1>
    <div>
      <input type="input" v-model="name" />
      <button @click="hello">send</button>
      <p>Message: {{ message }}</p>
    </div>
  </div>
</template>
<script>
import {HelloServiceClient} from "~/assets/_proto/hello_grpc_web_pb"
import {HelloRequest, HelloResponse} from "~/assets/_proto/hello_pb"
 export default {
  asyncData() {
    return {
      name: "",
      message: "",
    }
  },
  methods: {
    hello() {
      let req = new HelloRequest()
      req.setName(this.name)

      const client = new HelloServiceClient("https://127.0.0.1:8080", {}, {})
      client.hello(req, {}, (err, ret) => {
        this.message = ret.getMessage()
      })
    }
  }
}
</script>
```

`https://127.0.0.1:8080` 今回のProxyに対してgRPC通信を試みています。
なにか入力し、Sendボタンを押すと、「Hello ○○」というのが画面に表示されます。

まだProxyはないのでこれは動きません。

ちなみにこのアプリを動かすには以下のコマンドです。

```bash
$ cd client/
$ npm install
$ npm run dev

# ブラウザでアクセス
http://localhost:3000
```

## Nginxの構築
さぁ本題です。  

NginxはDockerでやりましょう。

```yaml:docker-compose.yaml
version: "3"
services:
  proxy:
    image: nginx:1.15.6-alpine
    ports:
      - "8080:8080"
    expose:
      - "8080"
    volumes:
      - ./proxy/nginx/common:/etc/nginx/common
      - ./proxy/nginx/conf.d:/etc/nginx/conf.d
      - ./proxy/keys:/ssl
```

```nginx:proxy/nginx/conf.d/sample.conf
server {
  listen 8080 ssl http2;
  server_name host.docker.internal;

  ssl_certificate /ssl/localhost+1.pem;
  ssl_certificate_key /ssl/localhost+1-key.pem;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;

  access_log /dev/stdout;
  error_log /dev/stderr debug;

  location ~ \.(html|js)$ {
    root /var/www/html;
  }
  location /hello.HelloService/Hello {
    grpc_set_header Content-Type application/grpc;
    grpc_pass host.docker.internal:5000;
    include common/cors.conf;
  }
}
```

ここの肝はここです

```nginx
  location /hello.HelloService/Hello {
    grpc_set_header Content-Type application/grpc;
    grpc_pass host.docker.internal:5000;
    include common/cors.conf;
  }
```

locationでサービス名と関数名を記載すると、それごとの設定ができるので
サービスや関数単位でアクセス制御などができます。

そして `grpc_set_header Content-Type application/grpc;` これがないとgRPC-WebからgRPCサーバーへのプロキシが正しくされません。

あとはローカル用の証明書も発行します。以下はMacの例です。

```bash
$ brew install mkcert
$ mkcert -install
$ mkcert localhost 127.0.0.1
$ mv localhost+1* proxy/keys/.
```

ではNginxも起動していきます。

```bash
$ docker-compose up
```

## 確認
ブラウザで作ったクライアントにアクセスします。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3135323330332f38393731613138342d626464322d656661362d383761622d3164396166666131346431642e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/152303/489ba9d0-7f2b-aa48-dced-69e8cc5beb3f.png)

「太郎」と入力したら、「Hello, 太郎」と返ってきました。
これでgRPC-WebとgRPCサーバーで疎通確認できました！

## 最後に
慣れ親しんだNginxでも無事使えることがわかりました。  
ただgRPC-Webは制限があり使用には注意が必要です。
[WebブラウザからgRPCの双方向通信は可能なのか調べた](https://qiita.com/Morix1500/items/7d9a152f8f12b73a1cf4)

現在僕は[double jump.tokyo株式会社](https://www.doublejump.tokyo/)様から gRPC基盤の構築の業務委託を受けて行っています。
その業務の一環としてこのような技術検証を行いました。