GoのgRPCサーバをGKEのIngressで使うために、GKEのHealth CheckがgRPCでないAPIを要求することの回避方法

## tl;dr

- Ingress の Health Check は HTTPS の GET リクエスト（gRPC ではない）で 200 を返さないといけない
- Go の gRPC サーバの実装に追加で、普通の HTTPS の GET リクエストで /healthz で 200 応答を返すようにする
- Pod の Readiness Probe を、HTTPS の GET リクエストのパス /helathz に設定すると、Health Check がそのパスに対して行われるようになる

## Ingress と gRPC の Health Check の問題とは

GKE を使ってサービスを公開する場合、L7 Load Balancer である Ingress を使ってサービスを公開する。
Ingress は GCP の Load Balancer を使って構築されている。

GCP の Load Balancer では、NEG を用いることでバックエンドのコンテナごとに Health Check が行われる。
この Health Check が通ったコンテナのみがリクエストを割り振られることになっている。

この Load Balancer の Health Check は HTTP(S) の GET リクエストを用いて行われる。
gRPC サーバにもかかわらず、Health Check のために HTTP(S) の GET リクエストで応答する必要がある。

[gRPCの公式チュートリアル](https://grpc.io/docs/languages/go/basics/)ではこの点までカバーされていない。

## gRPC + HTTP Health Check の実装例

Go で gRPC のアプリケーションを実装し（構造体 app に実装されているとする）、それに Health Check を追加したサーバの実装は以下のようになる。
HTTP ハンドラーの ServerHTTP() 関数において、リクエストが GET /healthz のみ即座に応答し、それ以外は gRPC サーバに流すようにする。

```go
package main

import (
	"context"
	"net/http"

	"google.golang.org/grpc"
	"google.golang.org/grpc/examples/route_guide/routeguide"
)

// gRPC のアプリを実装した構造体
type app struct{}

// Health Check 用のパスを追加した HTTPハンドラー
type httpHandler struct {
	grpcServer *grpc.Server
}

func (s *httpHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
	// GET /healthz のリクエストだけ、即座に 200 応答する
	if req.URL.Path == "/healthz" && req.Method == "GET" {
		res.WriteHeader(200)
		res.Write([]byte("OK"))
		return
	}
	// それ以外は gRPC サーバに流す
	s. grpcServer.ServeHTTP(res, req)
}

func main() {

	// gRPC
	grpcServer := grpc.NewServer()
	routeguide.RegisterRouteGuideServer(grpcServer, &app{})

	handler := &httpHandler{
		grpcServer: grpcServer,
	}
	http.ListenAndServeTLS(":443", "./server.crt", "./server.key", handler)
}
```

ここで使う SSL証明書は、Ingress からアクセスさせるために使う（HTTP2 では TLS 必須の様子）ためだけであるため、以下のコマンドを実行した自己証明書で構わない。
ただし、Ingress からは HTTP2 を指定した場合、必ず HTTPS が使われるため、自己証明書でも TLS 暗号化が必要である。

```
openssl genrsa 2048 > server.key
openssl req -new -key server.key > server.csr
openssl x509 -in server.csr -days 3650 -req -signkey server.key -sha256 > server.crt
```

## Manifests

Manifest の要点は以下のとおりである。

- Deployment の Readiness Probe で /healthz を指定する
- Service のアノテーションで、 HTTP2 であることを記述する

本筋ではないが、以下も行っている。

- GCP のマネージド SSL 証明書を、ドメイン（例:`temp.74th.tech`）で使用する
- NEG（コンテナ単位のロードバランシング）を有効にする（有効にしない場合、ノード単位のロードバランシングになる）
- Static IP Address を指定する（この IP アドレスで事前にドメインを解決できるように設定しているとする）

Static IP Address は以下の Google Cloud SDK のコマンドで取得できる。

```
gcloud compute addresses create grpc-ingress-ip --global
```

### Deployment

Deployment では、先の Health Check のパスを Readiness Probe で指定している。
TLS 暗号化をしているため、schema には HTTPS を指定する。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpc-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grpc-app
  template:
    metadata:
      labels:
        app: grpc-app
    spec:
      containers:
        - name: grpc-app
          image: 74th/grpc-app:latest
          ports:
            - containerPort: 443
          volumeMounts:
            - mountPath: /app/cert
              name: cert
          readinessProbe:
            httpGet:
              path: /healthz
              scheme: HTTPS
              port: 443
      volumes:
        - name: cert
          configMap:
            name: app-cert
```

### Service

Ingress を使う場合、Service には NodePort を使う。ここで、NEG の設定と、HTTP2 を使うように、アノテーションで指定する。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: grpc-app
  annotations:
    cloud.google.com/neg: '{"exposed_ports": {"443":{}}}'
    cloud.google.com/app-protocols: '{"export-port":"HTTP2"}'
spec:
  type: NodePort
  selector:
    app: proxy
  ports:
    - name: export-port
      protocol: TCP
      port: 443
      targetPort: 443
```

### GCP のマネージド SSL 証明書

GCP のマネージド SSL 証明書のリソースは、CRD で提供されているので、これも一緒に適用する。

```yaml
apiVersion: networking.gke.io/v1beta2
kind: ManagedCertificate
metadata:
  name: ingress-cert
spec:
  domains:
    - temp.74th.tech
```

### Ingress

Ingress では、先の Service の指定を行う。
Load Balancer で使われる Helath Check はここで解決できる Deployment の Readiness Tester の設定が使われるようになっている。

なお、Ingress の構築には、10 分程度時間がかかることが多い。Ingressで使われるSSL証明書は、最初マネージドSSLとして指定したものと異なるものが最初使われるが、しばらくすると指定したドメインのSSL証明書に置き換わるようになっている。

```yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: grpc-app
  annotations:
    kubernetes.io/ingress.allow-http: "false"
    kubernetes.io/ingress.global-static-ip-name: grpc-ingress-ip
    networking.gke.io/managed-certificates: ingress-cert
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: grpc-app
              servicePort: 443
```

## これ以外の方法

間に Envoy を挟み、Lua のスクリプトで GET / に対して 200 応答を返すようにしている例もある。

https://medium.com/google-cloud/gke-grpc-ingress-loadbalancing-4b9cdbc09758