この記事は GRIPHONE Advent Calendar 2020 23日目の記事です。
こんにちは、SREの川野です。
今回はGCPの共有VPCを使って、バックエンドにistio-ingressgatewayを置いたコンテナネイティブな負荷分散システム(NEG+Ingress)を構築する設定や、つまづいた点についてご説明します。
アーキテクチャについて
今回ご紹介する内容は以下の図のような構成になります。
バックエンドにistio-ingressgatewayを置いたコンテナネイティブな負荷分散システム(NEG+Ingress)を構築しています。
Ingressとは
IngressはKubernetesのネットワークリソースの1つです。GKE上でIngressリソースを作成するとGKEのIngress コントローラがGCLBを作成します。
Ingress オブジェクトを作成すると、GKE Ingress コントローラによって Cloud HTTP(S) ロードバランサが作成され、Ingress および関連する Service の情報に従ってそれが構成されます。
[HTTP(S) 負荷分散用 GKE Ingress | Kubernetes Engine ドキュメント | Google Cloud]
GKEを使用する上でざっくりといえば、Ingress ≒ ロードバランサ(GCLB)の認識で良いかと思います。
NEGとは
NetworkEndpointGroupのことで、エンドポイントやサービスを指定するオブジェクトです。
GCLBのバックエンドにインスタンスグループかNEGかといった指定の仕方ができるのですが、インスタンスグループを指定するのに対してNEGを指定すると、直接Podに対して負荷分散させることができるため、ネットワークホップが減少しレイテンシとスループットが改善されます。
[ネットワーク エンドポイント グループの概要 | 負荷分散 | Google Cloud]
[コンテナ ネイティブの負荷分散 | Kubernetes Engine ドキュメント | Google Cloud]
共有VPCとは
GCPの複数のプロジェクトでVPCを共有することができるというものです。
弊社ではGKEを管理するプロジェクトとVPC ネットワークを管理するプロジェクトを分けている構成をとっているため、共有VPCを使用しています。
共有VPCを使ったNEG+Ingress+istio-ingressgatewayの構成
istio-ingressgatewayの設定
弊社では、IstioOperatorを使ってIstioを導入しており、そのマニフェストが以下になります。以下のマニフェストを適用することで、istiod(istioのControl plane)とistio-ingressgatewayが作成されます。
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
name: istio
spec:
profile: default
addonComponents:
grafana:
enabled: false
meshConfig:
accessLogFile: /dev/stdout
accessLogEncoding: JSON
components:
ingressGateways:
- name: istio-ingressgateway
enabled: true
k8s:
hpaSpec:
minReplicas: 2
service:
type: ClusterIP
serviceAnnotations:
cloud.google.com/neg: '{"ingress": true}'
cloud.google.com/backend-config: '{"default":"istio-ingressgateway"}'
cloud.google.com/app-protocols: '{"https":"HTTPS","http2":"HTTP"}'
overlays:
- apiVersion: apps/v1
kind: Deployment
name: istio-ingressgateway
patches:
- path: spec.template.spec.readinessGates
value:
- conditionType: cloud.google.com/load-balancer-neg-ready
values:
pilot:
autoscaleMin: 2
このマニフェストでいくつかポイントを説明します。
serviceAnnotationsフィールドについて
serviceAnnotations:
cloud.google.com/neg: '{"ingress": true}'
cloud.google.com/backend-config: '{"default":"istio-ingressgateway"}'
cloud.google.com/app-protocols: '{"https":"HTTPS","http2":"HTTP"}'
cloud.google.com/neg: '{"ingress": true}'
↑こちらのアノテーションで、コンテナネイティブの負荷分散が有効化されます。Ingressが作成されたあとにNEGが作成されるので、作成済のIngressがあるときにNEGを作成したいという場合は、Ingressの削除と再作成が必要です。
cloud.google.com/backend-config: '{"default":"istio-ingressgateway"}'
↑こちらのアノテーションでは、以下のBackendConfigリソースを指定しています。そうするとIngressがBackendConfigを参照してくれるようになります。
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: istio-ingressgateway
spec:
healthCheck:
checkIntervalSec: 5
timeoutSec: 1
healthyThreshold: 1
unhealthyThreshold: 2
type: HTTP
requestPath: /healthz/ready
port: 15021
BackendConfigの内容では、GCLBがNEGに対して行うヘルスチェックの設定をしています。また、ヘルスチェックのファイアウォールルールをCloudProviderが自動で作成してくれます(v1.17.13-gke.2001で確認)。条件によっては手動でファイアウォールルールを作成する必要があるので注意が必要です。
・https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-features#configuring_ingress_features_through_backendconfig_parameters
・https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-features#associating_backendconfig_with_your_ingress
cloud.google.com/app-protocols: '{"https":"HTTPS","http2":"HTTP"}'
↑最後のこちらのアノテーションでは、GCLBとバックエンド間の通信に使用するプロトコルを指定しています。
overlaysフィールドについて
overlays:
- apiVersion: apps/v1
kind: Deployment
name: istio-ingressgateway
patches:
- path: spec.template.spec.readinessGates
value:
- conditionType: cloud.google.com/load-balancer-neg-ready
ここではIstioOperatorで生成されるリソースに対してoverlaysを使ってマニフェストを書き換えているのですが、DeploymentリソースにredinessGates フィールドを追加して、condition-Typeに cloud.google.com/load-balancer-neg-ready
を設定しています。これは、BackendConfigで設定したヘルスチェックが通り、正常にトラフィックを流せる状態になればIngressコントローラーがRediness Gates(下記の出力例参照)のSTATUS値をTrueに設定するという処理を行います。kubeletがこの値とPodに設定してあるreadinessProbeの両方を考慮し、トラフィックを流せる準備ができているか確認するようです。
Readiness Gates:
Type Status
cloud.google.com/load-balancer-neg-ready True
Ingressの設定
Ingressの設定で使用しているマニフェストが以下になります。
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: istio-ingressgateway
annotations:
kubernetes.io/ingress.allow-http: "false"
spec:
tls:
- secretName: certificate-platform-ingressgateway
rules:
- host: example.com
http:
paths:
- backend:
serviceName: istio-ingressgateway
servicePort: 80
annotationsフィールドでHTTPを無効化していますが、Podのistio-ingressgatewayの後ろで動いているアプリケーションでは、使用するクライアントが限られていることからHTTPSのみを使うようにしています。
backendフィールドでは、先ほど作成していたPodのistio-ingressgatewayをIngressのバックエンドとして指定しています。
つまづいたこと
GKEのサービスアカウントの権限が足りてなかった
Ingressの作成時にNEGを作成するのですが、そのタイミングでGKEのサービスアカウントがネットワークリソース周りの処理を行うようでForbiddenエラーが出ていました。Cloud LoggingやIngressのイベントログなどを確認しつつ、最終以下の権限がGKEのサービスアカウントに必要なことがわかりました。共有VPCを使っているので、ネットワークを管理している方のプロジェクトのIAMに以下の権限をもつカスタムロールを作成し、GKEサービスアカウントにバインディングすることで解決しました。
permissions = [
"compute.subnetworks.use",
"compute.firewalls.get",
"compute.firewalls.create",
"compute.firewalls.update",
"compute.firewalls.delete",
"compute.networks.updatePolicy",
]
GCLBでSNIがサポートされてなかった
最初は、Ingressのマニフェストのbackendフィールドで、下記のように servicePort:443
を指定してましたが、うまくいかず困っていました。この設定はGCLBとバックエンド間の通信で、どのサービスにどのポートで接続するかというものですが、GCLBではSNIがサポートされておらず、SNIの情報をTLS Clientとしてバックエンドには送らない仕様になっているようで、バックエンドで動いているistio-ingressgatewayが、SNI情報がないのでRouteがないと判断し通信を止めるという状態でした。
- backend:
serviceName: istio-ingressgateway
servicePort: 443
GFE は、バックエンドへの TLS セッションを開始する際に、Server Name Indication(SNI)拡張機能を使用しません。
https://cloud.google.com/load-balancing/docs/ssl-certificates#backend-encryption
対応策としては最初に紹介したIngressのマニフェストの通り、servicePort:80
に指定してGCLBとバックエンド間の通信はHTTPのみを使用するようにしています。
見出しの内容とは少しそれますが、バックエンド側ではHTTPSの通信を受け入れるようなGatewayの設定になっていて、当初はHTTPできたリクエストをHTTPSへリダイレクトするというものも入れていましたが、現状HTTPのみしかこずリダイレクト祭になってしまうのでリダイレクトさせる設定を外す対応をしました。
おわりに
如何でしたでしょうか。
現状ではGCLBとバックエンド間の通信が暗号化されていないのですが、いずれGCLBでSNIがサポートされると良いなあという風に思っています。
似たような構成を考えてる方の参考になれば幸いです。