NginxにgRPCとHTTP/1.1の共存の設定をする 

この記事はZOZOテクノロジーズ 4 Advent Calendar 2019 2日目の記事になります。昨日は @ararajp さんの「[評価する際のマイルール ](https://qiita.com/advent-calendar/2019/zozo_tech4)」でした。 
# はじめに
ZOZOの生産チームでは、Go言語でバックエンドのAPI開発をしています。今期になってgRPCを用いたAPIの開発を行おうということになり、既存のRESTのAPIとの共存ができるのかという課題がでてきました。その際に行った実装と検証について書こうと思います。
Nginxの1.13.10からgRPCがサポートされています（最新のバージョンは執筆時1.17です）。基本的には公式サイトのExample実装を弊社プロジェクトに置き換えるという流れで実装と検証をすすめました。
[Introducing gRPC Support with NGINX](https://www.nginx.com/blog/nginx-1-13-10-grpc/)

# gRPCとは
Google社が開発をした通信プロトコルの１つです。高速に通信できたり非同期データ通信などが可能になる特徴があります。HTTP2を利用しています。gRPCについての詳細は、公式サイトの https://grpc.io/ にあります。
# Nginxとは
Webサーバへのリクエストをさばくソフトです。同じ仕事をする仲間で、ApacheとかIISとかあります。
一番の特徴としてリバースプロキシという機能があります。この機能を使って、HTTPとHTTP2 (gRPC) のリクエストを振り分けをするわけです。

# 実現したいこと
<img width="823" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535399/7df75d00-1d46-0319-8919-24784f1004a6.png">

# 動作環境
- Macbook Pro
- docker 
- go
- grpcurl：gRPCリクエスト用
- Postman：HTTPリクエスト用

# Nginx設定

```sh:default.conf
upstream api-app {
    server api:1188;
}

upstream mikan-grpc-app {
    server grpc:50051;
}

server {
    listen          80 default_server;
    listen          50055 http2 default_server;
    server_name     _;

    real_ip_header     X-Forwarded-For;
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;

    location / {
        access_log /var/log/nginx/access.log main;
        grpc_pass grpc://mikan-grpc-app;
    }

    location /api {
        access_log /var/log/nginx/access.log main;
        proxy_pass http://api-app;
    }
}
```

##### `upstream`：HTTP1用とHTTP2用をそれぞれ2つ定義しています。

|    upstream  |説明|
|----------------|--------------------------|
|api-app:1188|HTTPリクエストを処理するサーバ|
|mikan-grpc-app:50051 |HTTP2リクエストを処理するサーバ|

##### `server`ブロックのListenの設定

|    Listenポート  |説明|
|----------------|---------------|
|80|通常のHTTPリクエストを待ち受ける|
|50055|HTTP2リクエスト（gRPC）を待ち受ける|

##### `location`：リクエストに対してのプロキシ設定

|    パス  |説明|
|----------------|---------------|
|`/`|デフォルトはすべてgRPCで処理されるようする|
|`/api`|`api`が含まれる場合は、通常のHTTPリクエストとして処理される|

##### プロトコル・パス設定
`grpc_pass`はgRPC用、`proxy_pass`は通常のHTTPリクエスト用。
* プロトコル・スキームが間違いやすいので注意する

# docker-compose
APIサーバとgRPCサーバ、Nginxを設定します。

リクエストのListen設定は
* HTTPリクエスト(api)：8080
* HTTP2リクエスト(grpc)：50055

```yaml:doker-compose.yml
version: "3"
services:
  api:
    build:
      context: .
      dockerfile: "docker/Dockerfile.api"
    volumes:
      - .:/api
    ports:
      - 1188:1188
  grpc:
    build:
      context: .
      dockerfile: "docker/Dockerfile.mikan"
    volumes:
      - .:/grpcapi
    ports:
      - 50051:50051
  nginx:
    build:
      context: .
      dockerfile: "docker/Dockerfile.nginx"
    volumes:
      - "./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro"
      - "./nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
    links:
      - "api"
      - "grpc"
    ports:
      - 8080:80
      - 50055:50055
```

# 構成略図

<img width="815" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4181/05898447-3e27-4110-7c0e-ae3e8e67bde6.png">


# 動作確認
dockerで確認してみます。

```shell-session
# docker-compose up --build

api_1    | 
api_1    |    ____    __
api_1    |   / __/___/ /  ___
api_1    |  / _// __/ _ \/ _ \
api_1    | /___/\__/_//_/\___/ v3.3.10-dev
api_1    | High performance, minimalist Go web framework
api_1    | https://echo.labstack.com
api_1    | ____________________________________O/_______
api_1    |                                     O\
api_1    | ⇨ http server started on [::]:1188
grpc_1   | Starting Mikan Service
```
api(echo)とgrpcが起動していればOKです。

### PostmanでHTTPリクエストする

リクエストするURL：http://localhost:8080/api/ping

```shell-session
nginx_1  | {"date_time": "2019-11-20T05:37:13+00:00","time_ms": "1574228233.287","ip": "","host": "localhost","req_time": "0.006","ups_time": "0.010","run_time": "","status": "200","bytes_recv": "262","method": "GET","request_body": ""}
```
と`Nginx`のログが出力されれば、リクエストが期待通りにプロキシされているという確認ができました。

### gRPCのリクエストする
gRPCクライアントを実装するという選択肢もありますが、今回は、`grpcurl`というツールを使います。
docker版などもありますが、macだとHomebrewなどでインストールできます。[grpcurl](https://formulae.brew.sh/formula/grpcurl)
簡単にgRPCのリクエストをエミュレートできます。

別ターミナルからgRPCのリクエストをします。リクエストは[`.proto`](https://github.com/nakashimanh/mikans/blob/master/mikanpb/mikan.proto)に合わせてリクエストするので、例えばこんな感じになります。

```shell_session
grpcurl -plaintext -d '{"mikan": {"Name": "Request Mikan", "Kind": "Good", "Quality": 16} }' localhost:50055 mikan.MikanService/Mikan
```
grpcurlの使い方もいろいろありますが、上記の例を説明すると

|    項目  |説明|
|----------------|--------------------------|
|localhost:50055|NginxがListenしているgRPCリクエストのポート|
|-d|送信データ（.protoの定義によって変わります）|
|mikan.MikanService/Mikan|MikanServiceのMikanという関数・機能|
という内容です。
*後述のgRPCサーバのコード内に定義してあります。

```shell_session
grpc_1   | Mikan function was invoked with mikan:<Name:"Request Mikan" Kind:"Good" Quality:16 > 
nginx_1  | {"date_time": "2019-11-20T06:04:58+00:00","time_ms": "1574229898.868","ip": "","host": "localhost","req_time": "0.001","ups_time": "0.000","run_time": "","status": "200","bytes_recv": "63","method": "POST","request_body": ""}
```
NginxのアクセスログとgRPCの出力が表示されていれば、期待通りのプロキシがされたという確認ができました。

##### 注意
`grpcurl`で`50051`ポートでもgRPCサーバはレスポンスが返りますが、`Nginx`を経由していないリクエストになります。`Nginx`の恩恵を受けるには必ず`Nginx`側の`Listen`ポート(50055)からのアクセスが必要です。

# 補足：APIサーバ
リクエストパスは`api`以下として、1188ポートでHTTPのリクエストを受けるように設定します。

```golang:server.go
package main

import (
	"net/http"
	"time"

	"github.com/labstack/echo"
)

func main() {
	e := echo.New()

	e.GET("/api/", func(c echo.Context) error {
		return c.String(http.StatusOK, "API-/")
	})

	e.GET("/api/ping", func(c echo.Context) error {
		layout := "2006-01-02 15:04:05"
		loc, _ := time.LoadLocation("Asia/Tokyo")
		now := time.Now().In(loc).Format(layout)
		return c.String(http.StatusOK, "API:Ping - "+now)
	})

	e.Logger.Fatal(e.Start(":1188"))
}
```


# 補足：gRPCサーバ
リクエストパスは`/`以下をすべてとして、ポートは`50051`でリクエストを受けます。

```golang:server.go
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"strconv"

	"github.com/nakashimanh/mikans/mikanpb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

type server struct{}

func (*server) Mikan(ctx context.Context, req *mikanpb.MikanRequest) (*mikanpb.MikanResponse, error) {
	fmt.Printf("Mikan function was invoked with %v\n", req)
	name := req.GetMikan().GetName()
	kind := req.GetMikan().GetKind()
	quality := req.GetMikan().GetQuality()
	result := "Response Mikan= " + name + " kind= " + kind + " quality= " + strconv.FormatInt(quality, 10)
	res := &mikanpb.MikanResponse{
		Result: result,
	}
	return res, nil
}

func main() {
	fmt.Println("Starting Mikan Service")
	lis, err := net.Listen("tcp", "0.0.0.0:50051")
	if err != nil {
		log.Fatalf("Failed to listen: %v", err)
	}
	s := grpc.NewServer()
	mikanpb.RegisterMikanServiceServer(s, &server{})
	reflection.Register(s)

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

詳しく知りたい方は、ソースコード：[こちら](https://github.com/nakashimanh/mikans) で確認できます。
gRPCサーバの構築については、[こちらの記事](https://zenn.dev/mikankitten/articles/0437fa6fb7de82) を御覧ください。

# まとめ
Nginxの公式サイトなどをみながら、試行錯誤しながらなんとか動作確認までできました。
gRPCをはじめ、使い慣れていないツールなどもあり、色々と詰まる局面もありましたが、Nginxの利点を生かせるサービスができるようになってよかったです。また、本格的なマイクロサービス化への第1歩になりそう。
今回は、Nginxの話だったので、dockerとかgRPCの内容はさらっと書いてます。詳しくはレポジトリにコードあります。
最後に、本番環境での確認については書いていませんが、それぞれのデプロイ先の環境に合わせる必要があるため、必ず動作確認してからリリースしましょう！
:santa: 楽しいクリスマスをお過ごしください！:evergreen_tree:
明日は @ikeponsu さんによる「[GASでGoogleドライブ監視システムを作成](https://qiita.com/advent-calendar/2019/zozo_tech4)」です。