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の公式チュートリアルではこの点までカバーされていない。
gRPC + HTTP Health Check の実装例
Go で gRPC のアプリケーションを実装し(構造体 app に実装されているとする)、それに Health Check を追加したサーバの実装は以下のようになる。
HTTP ハンドラーの ServerHTTP() 関数において、リクエストが GET /healthz のみ即座に応答し、それ以外は gRPC サーバに流すようにする。
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 を指定する。
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 を使うように、アノテーションで指定する。
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 で提供されているので、これも一緒に適用する。
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証明書に置き換わるようになっている。
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 応答を返すようにしている例もある。