GKE Ingress + gRPC アプリケーションのヘルスチェックをどうにかする
この記事は Google Cloud Japan Customer Engineer Advent Calendar 2019 の 9 日目の記事です。
TL;DR
- GKE Ingress + gRPC アプリケーションはヘルスチェックに一工夫必要
- あえて Nginx を使って解決してみる
- すぐに使えるコマンド例と設定ファイルつき
HTTP(S) ロードバランサーのヘルスチェックは gRPC に対応していない
近頃使われることが多くなってきた gRPC ですが、GCP の HTTP(S) ロードバランサーは gRPC でヘルスチェックを行うことができません。
これは HTTP(S) ロードバランサーを Ingress として使う GKE で「GKE Ingress + gRPC アプリケーション」をデプロイしようとしても、そのままではヘルスチェックが通らないため、クライアントからのリクエストがアプリケーションへ転送されないということを意味します。
どう対応するか?
GKE Ingress からのヘルスチェックは HTTP/2 で行われることになるため、これに対して正常なステータスを返す必要があり、現状では 3 つの方法が考えられます。
- アプリケーションに HTTP/2 で応答するヘルスチェック用のエンドポイントを追加する
- アプリケーション Pod の前段にリバースプロキシ Pod を置きヘルスチェックに応答する(gRPC リクエストはリバースプロキシ Pod からアプリケーション Pod に転送する)
- アプリケーションの Pod にサイドカーとしてリバースプロキシコンテナを配置してヘルスチェックに応答する(gRPC リクエストはリバースプロキシコンテナからアプリケーションコンテナに転送する)
この記事ではアプリケーションの変更が不要でサイドカーを入れるだけの 「3」の方法を、個人的に好きな Nginx を使ってやってみます。
手順
GKE クラスタの作成
さくっと gcloud コマンドで GKE クラスタを作成します。
ちなみに gcloud コマンドの実行は Cloud Shell からが便利です。
Cloud Shell には gcloud コマンドの他にも git や kubectl など GKE で何かをするときに必要となるコマンドが一通り揃っています : )
$ gcloud container clusters create my-cluster \
--num-nodes=1 \
--zone=asia-northeast1-a
サーバ証明書の作成
ドキュメントに以下のような記載があるとおり、HTTP/2 のヘルスチェックリクエストは TLS で行われるので、サーバ証明書を用意します。
If the backend service is configured to use an HTTPS or HTTP/2 health check, the request is encrypted on its way to the backend instance.
またこちらに以下の記載があるとおり、ヘルスチェックは証明書の検証を行わないため自己署名証明書を使用することができます。
Google Cloud health check probe systems do not perform certificate validation
ここでは自己署名証明書を使用しますが、Production 環境ではもちろんちゃんとしたものを使用してください。
秘密鍵の作成
$ openssl genrsa -out self-signed.key 2048
Certificate Signing Request の作成
$ openssl req -new -key self-signed.key -out self-signed.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Minato-ku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:GCJ
Organizational Unit Name (eg, section) []:CE
Common Name (e.g. server FQDN or YOUR name) []:*.example.com
Email Address []:Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
証明書の作成
$ openssl x509 -req -days 3650 -in self-signed.csr -signkey self-signed.key -out self-signed.crt
Kubernetes Secret の作成
作成した証明書と秘密鍵を Kubernetes の Secret オブジェクトにします。
この Secret オブジェクトを GKE Ingress と Nginx コンテナで使用します。
$ kubectl create secret tls tls-cert \
--cert=self-signed.crt \
--key=self-signed.key
作成した Secret オブジェクトを describe してファイル名を確認します。
(ここでは tls.crt
と tls.key
になります)
$ kubectl describe secret tls-cert
Name: tls-cert
Namespace: default
Labels: <none>
Annotations: <none>Type: kubernetes.io/tlsData
====
tls.crt: 1192 bytes
tls.key: 1675 bytes
Nginx 設定ファイルの作成
リバースプロキシコンテナには Nginx のオフィシャルイメージを使用します。
オフィシャルイメージの Nginx は /etc/nginx/nginx.conf
から /etc/nginx/conf.d/*.conf
をインクルードするように設定されていて、 /etc/nginx/conf.d
には default.conf
のみが存在しています。
今回はこの default.conf
を変更して HTTP/2 ヘルスチェックと gRPC アプリケーションのリクエストに対応できるようにしてみます。
以下の内容で default.conf
を作成します。
server {
listen 443 ssl http2; ssl_certificate tls/tls.crt;
ssl_certificate_key tls/tls.key; # for health check from load balancer
location = / {
return 200;
} # for gRPC Application
location / {
grpc_pass grpc://localhost:9000;
}
}
ssl_certificate
とssl_certificate_key
には上で確認した Secret オブジェクト内のファイル名を指定します。
また相対パスでtls
ディレクトリを指定していますが、これは Secret オブジェクトをこのパスにマウントするためです(後述)location
ディレクティブを使用して、ヘルスチェックリクエストとそれ以外のリクエスト(gRPC アプリケーションへのリクエスト)を処理しています。
/
宛に来るヘルスチェックリクエストには 200 OK を返し、それ以外のパスへのリクエストはgrpc_pass
ディレクティブを使用してアプリケーションコンテナに転送します。
Kubernetes ConfigMap の作成
作成した Nginx の設定ファイルから ConfigMap を作成します。
変更頻度の多い設定ファイルを ConfigMap にすることで、コンテナイメージから切り離すことができます。
こうすることで、設定ファイルの変更のためだけにコンテナイメージの再作成を行わずにすむなど管理上のメリットが生まれます。
ConfigMap は以下の様にファイルを指定して作成することができます。
$ kubectl create configmap nginx-conf --from-file=default.conf
Kubernetes Deployment の作成
gRPC アプリケーションとして gRPC echo サーバを、リバースプロキシコンテナには上述したように Nginx を使用しています。
以下の内容で deployment.yaml
を作成します。
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: yages
spec:
replicas: 1
template:
metadata:
labels:
app: yages
spec:
containers:
- name: nginx
image: nginx:1.17-alpine
ports:
- containerPort: 443
protocol: TCP
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: tls-cert
mountPath: /etc/nginx/tls
- name: yages
image: quay.io/mhausenblas/yages:0.1.0
ports:
- containerPort: 9000
protocol: TCP
volumes:
- name: nginx-conf
configMap:
name: nginx-conf
- name: tls-cert
secret:
secretName: tls-cert
volumes
を使用して上記で作成した Kubernetes の Secret と ConfigMap を Pod にアタッチしています。- nginx コンテナでは
volumeMounts
を使用して Secret オブジェクト内のサーバ証明書 / 秘密鍵とConfigMap オブジェクト内の Nginx 設定ファイルをコンテナ内の指定のパス(/etc/nginx/tls
,/etc/nginx/conf.d
)へマウントしています。
作成したマニフェストは以下のようにして Kubernetes に適用します。
$ kubectl apply -f deployment.yaml
Kubernetes Service の作成
Deployment に対して Service を作成していきます。
ポイントは HTTP/2 であることを annotations
を使って設定しているところです。
また、GKE Ingress をこの上に作成する場合、Service タイプは NodePort
である必要があります。
以下の内容で service.yaml を作成します。
apiVersion: v1
kind: Service
metadata:
annotations:
cloud.google.com/app-protocols: '{"my-port":"HTTP2"}'
name: yages
spec:
type: NodePort
ports:
- name: my-port
port: 443
targetPort: 443
protocol: TCP
selector:
app: yages
Deployment と同じように以下のコマンドで Kubernetes に適用します。
$ kubectl apply -f service.yaml
Kubernetes Ingress の作成
最後に Ingress を作成して外部からアクセスできるようにします。
GKE Ingress となる HTTP(S) Load Balancer の TLS 証明書として、上で作成した Secret オブジェクトを指定しています。
この記事では簡単のためにリバースプロキシコンテナと同じ Secret オブジェクトを再利用していますが、必ず同じにする必要はありません。
以下の内容で ingress.yaml を作成します。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: yages
spec:
tls:
- secretName: tls-cert
backend:
serviceName: yages
servicePort: 443
Deployment, Service と同じように以下のコマンドで Kubernetes に適用します。
$ kubectl apply -f ingress.yaml
動作確認
一通りデプロイが完了したので、結果を見ていきましょう。
まずは問題のヘルスチェックがどうなったかが以下の画像になります。
追加したリバースプロキシコンテナがヘルスチェックに対してきちんと応答しているようです。
GKE 上のコンテナログは Stackdriver Logging から確認することができます。(このように GKE は 様々なGCP サービスと統合されていて、その便利な機能を使えるのもポイントです)
実際にリバースプロキシコンテナである Nginx のアクセスログを Stackdriver Logging で見てみると、アクセスログからもヘルスチェックに対してきちんと応答を返せていることがわかります。
次に gRPC アプリケーションへのリクエストもちゃんと通るかを確認してみます。
ここでは、gRPC アプリケーションとして利用した gRPC echo サーバの README にも書かれていた gRPCurl を使ってみます。
まずはアクセス先となる Ingress の External IP を調べて、
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
yages * 34.102.214.64 80, 443 24h
判明した IP アドレス (34.102.214.64) に対して gRPCurl を実行します。
(作成した証明書のコモンネームにあわせて servername
オプションを指定します。またこの記事では自己署名証明書をしようしているので、 insecure
オプションを使用して証明書の検証をしないようにしています。)
$ grpcurl -servername yages.example.com -insecure 34.102.214.64:443 yages.Echo.Ping
{
"text": "pong"
}
gRPC アプリケーションへのリクエストも問題なく行えることが確認できました!
おわりに
GKE Ingress + gRPC アプリケーションを動かす際のヘルスチェックについて書いてみましたがいかがでしたでしょうか?
リバースプロキシコンテナを Pod に追加してあげる必要がありますが、単純にヘルスチェックリクエストとそれ以外を振り分けるだけであれば、比較的簡単に設定ファイルを書くことができます。(もちろん Production で動かす時にはもっとたくさんの設定が必要になります。)
様々な理由で gRPC アプリケーションにヘルスチェック用のエンドポイントを追加できない時などにこの記事が参考になればと思います。