k8sで、HTTP/2(gRPCサーバ)を負荷分散したい

# kubernetesで、HTTP/2を負荷分散してみた

## はじめに

### 背景
現在学生プロジェクトでインフラとして、GKE(Google Kubernetes Engine)を利用させて頂いています。
**REST APIサーバを、gRPCに換装**してみようとした時にある問題にぶち当たりました。

### 前提知識として
#### Kubernetesのロードバランサ
`Services are a “layer 4” (TCP/UDP over IP) construct, the proxy was purely in userspace`
現行のKubernetesのIngressは、OSI参照モデルのトランスポート層(L4)のロードバランサを提供しています。(beta版はその限りではないです！)
#### gRPCの通信の仕方
gRPCでの通信は、 **一つのTCPコネクションを使い回します**。
つまり、コネクションが確立している間はサーバとクライアントは繋がり続けます。

### 問題とは
`gRPC is a modern RPC protocol implemented on top of HTTP/2. HTTP/2 is a Layer 7 (Application layer) protocol` (参照: 公式サイトより)
というように、gRPCは、アプリケーション層(L7)で動作するのです。
つまり、L4のロードバランサでは、 **適切に負荷分散できない場合** があるのです！
Ingrees使えないじゃん！

### Envoyに出会いました
L7 LoadBalancerと調べていたら記事が見つかって来ました。
ref:  [kubernetesでgRPCするときにenvoy挟んでみたよ](https://qiita.com/wataru420/items/a63a8b205308a93e253c)
**Envoy**とは、クラウドネイティブ向けつまりマイクロサービス向けのProxyとして設計されたオープンソースのソフトです。
**L7プロキシももちろん対応です！**
<img width="200" src="https://qiita-image-store.s3.amazonaws.com/0/192927/53b64fbb-0a25-ca32-0753-90a17e0105ab.png">

## セットアップ
では早速導入していきましょう！
流れはこんな感じです。
1. gRPCサーバのServiceを**Load Balancer**から**Headless Service**にする
2. Envoyをクラスタにデプロイし**外部公開**

### gRPCサーバのServiceをLoad BalancerからHeadless Serviceにする
#### Envoyのコンフィグファイルはこんな感じになっています。

```
apiVersion: v1
kind: ConfigMap
metadata:
  name: "envoy-config"
data:
  envoy.json: |
    {
      "listeners": [
        {
          "address": "tcp://0.0.0.0:15001",
          "filters": [
            {
              "type": "read",
              "name": "http_connection_manager",
              "config": {
                "codec_type": "auto",
                "stat_prefix": "ingress_http",
                "route_config": {
                  "virtual_hosts": [
                    {
                      "name": "service",
                      "domains": ["*"],
                      "routes": [
                        {
                          "timeout_ms": 0,
                          "prefix": "/",
                          "cluster": ""
                        }
                      ]
                    }
                  ]
                },
                "filters": [
                  {
                    "type": "decoder",
                    "name": "router",
                    "config": {}
                  }
                ]
              }
            }
          ]
        }
      ],
      "admin": {
       "access_log_path": "/dev/stdout",
       "address": "tcp://127.0.0.1:8001"
      },
      "cluster_manager": {
        "clusters": [
          {
            "name": "rakusale-grpc",
            "features": "http2",
            "connect_timeout_ms": 250,
            "type": "strict_dns",
            "lb_type": "round_robin",
            "hosts": [{"url": "tcp://Host名:Port番号"}]
         }
        ]
      }
    }
```

#### 注目して欲しいところはここです！

```
"cluster_manager": {
        "clusters": [
          {
            "name": "rakusale-grpc",
            "features": "http2",
            "connect_timeout_ms": 250,
            "type": "strict_dns",
            "lb_type": "round_robin",
            "hosts": [{"url": "tcp://Host名:Port番号"}]
         }
        ]
      }
```
**clusters["hosts"]のところに、サーバ側のIPとポートを指定**します。
つまり、Envoy側が負荷分散するために**PodのIP**を知っている必要があります。

#### ここで、Headless Service
Headless Serviceは、Serviceに対してDNSリクエストを送るとPodのIPを返してくれるものです。
動かしたいサーバのサービスはこのように設定しています。

```
apiVersion: v1
kind: Service
metadata:
  labels: 
    name: ""
  name: ""
spec:
  clusterIP: None
  ports:
    - name: ""
      port: 3001
      protocol: TCP
      targetPort: 3001
  selector:
    app: ""
```
ClusterIP: Noneとするだけで、Headless Serviceになります！

### Envoyをクラスタにデプロイし外部公開
必要なものは3つです。
- envoy_configmap.yaml : さっき紹介しています！
- envoy_deployment.yaml
- envoy_service.yaml

#### envoy_deployment.yaml
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: "envoy"
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: "envoy"
    spec:
      volumes:
        - name: envoy
          configMap:
            name: "envoy-config"
        # - name: tls
        #   secret:
        #     secretName: tlssecret

      containers:
        - name: envoy
          image: envoyproxy/envoy:6e3633496f5a9412abdca8bac7db6b701ae8ce14
          command:
            - "/usr/local/bin/envoy"
          args:
            - "--config-path /etc/envoy/envoy.json"
          resources:
            limits:
              memory: 512Mi
          ports:
            - containerPort: 15001
              name: app
            - containerPort: 8001
              name: envoy-admin
          volumeMounts:
            - name: envoy
              mountPath: /etc/envoy
            # - name: tls
            #   mountPath: /etc/tlssecret
            #   readOnly: true
```

#### envoy_service.yaml
```
apiVersion: v1
kind: Service
metadata:
  labels:
    name: "envoy"
  name: "envoy-service"
spec:
  type: LoadBalancer
  selector:
    app: "envoy"
  ports:
    - name: tcp
      port: 15001
      targetPort: 15001
```
以上を用意して `kubectl apply` してGIPに接続すれば、負荷分散が完成しているはずです！

## 終わりに
Kubernetesクラスタ内のパケットとかネットワーク監視するツールで良いのがあったら教えて欲しいです！
あと、gRPCサーバ書く時にログをきちんと書く実装にしないと心底不便だなと思いました、、、
いつもはフレームワークがやってくれていたので、、、
今後も精進していきます