共有VPCを使ったNEG+Ingress+istio-ingressgatewayの設定

AvatarPosted by

この記事は 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 の概要  |  Google Cloud]

共有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の削除と再作成が必要です。

https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing?hl=ja#create_service

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とバックエンド間の通信に使用するプロトコルを指定しています。

https://cloud.google.com/kubernetes-engine/docs/concepts/ingress-xlb#https_tls_between_load_balancer_and_your_application

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

https://cloud.google.com/kubernetes-engine/docs/concepts/container-native-load-balancing?hl=ja#pod_readiness

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がサポートされると良いなあという風に思っています。

似たような構成を考えてる方の参考になれば幸いです。