🔀

GKE Gateway controller に HTTP(S) ロードバランサの Gateway class が追加されたので検証してみた

2023/01/11に公開

こんにちは。クラウドエースの阿部です。
こちらの記事では、2022年12月16日に GKE Gateway controllerに追加された新しい HTTP(S) ロードバランサの Gateway class について検証してみます。

はじめに

Kubernetes Gateway API と GKE Gateway controller の概要や基本構成については、下記の記事にて説明しておりますので、よろしければご一読ください。

https://zenn.dev/cloud_ace/articles/255ccf620f7707

追加された機能について

現在、Google Cloud で使用可能な外部HTTP(S)ロードバランサは3種類ありますが、前述のブログを記述している時点では、GKE Gateway controllerでは以下のの2つのみをサポートしていました。

  • グローバル外部 HTTP(S) ロードバランサ (従来)
  • リージョナル外部 HTTP(S) ロードバランサ

この2つに加え、2022年12月16日に「グローバル外部 HTTP(S) ロードバランサ」が GKE Gateway controller で利用可能になりました。
「グローバル外部 HTTP(S) ロードバランサ (従来)」と「グローバル外部 HTTP(S) ロードバランサ」は名前こそ似ていますが、機能としては違いがあり、高度なトラフィック管理(トラフィック分割やミラーリング)の機能は従来のロードバランサには存在しない機能です。
(ちなみに、リージョナル外部 HTTP(S) ロードバランサも高度なトラフィック管理の機能を有しています。そのため、本記事で動作確認している手順は同様に使用可能のはずです。)

これらをどのように使っていくかについて説明していきたいと思います。

使い方

環境準備

環境準備の手順ですが、基本的には以前のブログ記事で説明した手順と変わらないため、下記ブログの「GKEクラスタのセットアップ」を読んで頂ければと思います。

https://zenn.dev/cloud_ace/articles/255ccf620f7707#gkeクラスタのセットアップ

以降の手順は、「GKEクラスタのセットアップ」の手順で作成したクラスタがある前提で説明していきます。
手順通り作成すると、以下のリソースが事前にできあがっているはずです。

  • グローバル外部IPアドレス
  • Cloud Endpoints によるFQDN (グローバル外部IPアドレスに対応)
  • マネージド証明書リソース (名前は gke-gateway-cert、上記のFQDNに対応)
  • GKEクラスタ (クラスタのバージョンはv1.24.5以降)

環境準備が完了したら、下記のコマンドを実行してみましょう。

kubectl get gatewayclass

以下のように、 gke-l7-global-external-managed が表示されていると思います。これが、今回新しく追加されたロードバランサの Gateway class です。

実行結果
NAME                             CONTROLLER                  ACCEPTED   AGE  
gke-l7-global-external-managed   networking.gke.io/gateway   True       5h11m
gke-l7-gxlb                      networking.gke.io/gateway   True       5h11m
gke-l7-rilb                      networking.gke.io/gateway   True       5h11m

デモアプリケーションのデプロイ

デモアプリケーションとして、Google公式リファレンスにあるデモアプリケーションをデプロイします。

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/gke-networking-recipes/main/gateway/gke-gateway-controller/app/store.yaml

上記コマンド実行により、default Namespace に store-v1store-v2store-german DeploymentとServiceがそれぞれデプロイされます。

Gateway リソースのデプロイ

今回デプロイする Gateway リソースのマニフェストは以下の通りです。

gateway.yaml
gateway.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: external-https
spec:
  gatewayClassName: gke-l7-global-external-managed
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        options:
          networking.gke.io/pre-shared-certs: gke-gateway-cert
  addresses:
    - type: NamedAddress
      value: gke-gateway-vip

前回のブログ記事との違いとしては、 gatewayClassName に gke-l7-global-external-managed を指定している点です。これにより、グローバル外部 HTTP(S) ロードバランサは従来ではなく新しい方が使用されます。
上記のマニフェストをデプロイします。

kubectl apply -f gateway.yaml

トラフィック分割の動作を確認

トラフィック分割とは、重み設定に従って複数のサービスにトラフィックを分割して処理する設定です。
Kubernetes Gateway API では、トラフィックの重みを HTTPRoute リソースで設定します。

カナリアリリース設定

今回は、カナリアリリースすることを想定して、 store-v1 に95%、 store-v2 に5%のトラフィックを振り分ける設定を行います。
以下のような store.yaml を作成します。ポイントとしては、 backendRefs 設定に weight を追加したことです。

store.yaml
store.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: store
spec:
  parentRefs:
    - kind: Gateway
      name: external-https
  hostnames:
    - "{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog"
  rules:
    - backendRefs:
        - name: store-v1
          port: 8080
          weight: 95
        - name: store-v2
          port: 8080
          weight: 5

{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog は Cloud Endpoints で設定したFQDNです

上記のマニフェストをデプロイします。

kubectl apply -f store.yaml

※デプロイ後、マネージドSSL証明書やロードバランサの初期化に時間がかかるのでしばらく待ちましょう。

しばらくしたら、下記のコマンドを実行して store-v1store-v2にトラフィック分割されているかを観測してみます。
下記のコマンドを実行してみます。

for CNT in $(seq 1 1000); do curl -sS https://{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog/ | jq -r .metadata; done | sort | uniq -c

{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog の部分は Cloud Endpoints で作成したFQDN

上記のコマンドは、以下の内容を実行しています。

  1. Gateway controller で作成したエンドポイントに curl コマンドで1000回リクエスト
  2. 実行結果を jq コマンドで受け取り、 metadata の部分だけ抜き出す
  3. 抜き出した結果を sort コマンドで整列し、 uniq -c コマンドで件数を集計

結果は以下の通りでした。多少ズレはありますが、だいたい95:5の割合でトラフィックが分割されていることが分かりました。

実行結果
    958 store-v1
     42 store-v2

トラフィック分割設定の注意事項

HTTPRoute の weight にどんな値が設定可能かを確認したところ、 0から1000までの数値でした。これは、Google Cloud の URL Map API における weight の設定範囲と同一です。Gateway controller が裏で URL Map リソースを作成しているので、当たり前と言えば当たり前ですが、設定するときは注意しましょう。
weight がAPIの設定範囲を超えている場合は、HTTPRoute リソース自体は作成されますが、リソースの内部状態がエラーになります。
なお、各バックエンドの weight 合計値は100や1000に収まっていなくても問題有りませんでした。内部で合計値を計算した上でトラフィックの重みを設定しているようです。ただし、考え無しに設定してしまうと思ったようなトラフィック分割にならない状況に陥りがちになると思いますので、 weight の合計値は100または1000で設計した方がよいでしょう。

クリーンアップ

次の検証に行く前に、 store HTTPRoute リソースを削除しておきましょう。

kubectl delete httproute store

トラフィックミラーリングの動作を確認

トラフィックミラーリングとは、あるサービスへのトラフィックを複製して別のサービスにも送信する機能です。例えば本番のトラフィックを使った動作テストをやりたい場合や、トラフィックの監査目的等で使う機能です。

トラフィックミラーリング設定

以下のような store-mirror.yaml を作成します。ポイントとしては、 backendRefs 設定と並列に filters を追加して、 RequestMirror を設定したことです。

store-mirror.yaml
store-mirror.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: http-filter-mirroring
spec:
  parentRefs:
    - kind: Gateway
      name: external-https
  rules:
    - backendRefs:
        - name: store-v1
          port: 8080
      filters:
        - type: RequestMirror
          requestMirror:
            backendRef:
              name: store-v2
              port: 8080

上記のマニフェストをデプロイします。

kubectl apply -f store-mirror.yaml

デプロイ後、以下のコマンドを実行します。
まず、検証でコンテナのログ件数を確認しますが、各DeploymentのPod数は2になっているため件数がうまく数えられません。以下のコマンドで、Pod数を1に変更します。

kubectl scale --replicas=1 deployment store-v1
kubectl scale --replicas=1 deployment store-v2

少し待ってから kubectl get podkubectl get deployment を実行して store-v1store-v2のPod数が1つになっていることを確認します。(Pod数が確認できればよいのでどちらのコマンドを実行してもよいです。)

実際にトラフィックを発行するコマンドを実行します。 store-v1 に対して以下のコマンドを実行します。

for CNT in $(seq 1 100); do curl -sS https://{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog/ | jq -r .metadata; done | sort | uniq -c; \
  kubectl logs deployment/store-v1 --since=10s | grep -c INFO

上記のコマンドは、以下の内容を実行しています。つまり、 curl の実行件数とログの件数を比較するような操作です。

  1. Gateway controller で作成したエンドポイントに curl コマンドで100回リクエスト
  2. 実行結果を jq コマンドで受け取り、 metadata の部分だけ抜き出す
  3. 抜き出した結果を sort コマンドで整列し、 uniq -c コマンドで件数を集計
  4. 集計直後に kubectl logs コマンドで過去10秒分のログを抜き出し、 grep で行数を集計

実行結果は以下の通りでした。件数にずれがあるのは、Gateway controllerが設定したロードバランサのヘルスチェックのリクエストが余計に入ったためと思われますが、だいたい一致します。

実行結果 store-v1 のログとの比較
    100 store-v1
103

同様に、以下のコマンドを実行します。前述のコマンドとの違いは、ログ件数の集計対象を store-v2 にした部分です。

for CNT in $(seq 1 100); do curl -sS https://{SERVICENAME}.endpoints.{PROJECT_ID}.cloud.goog/ | jq -r .metadata; done | sort | uniq -c; \
  kubectl logs deployment/store-v2 --since=10s | grep -c INFO

実行結果は以下の通りでした。つまり、 store-v1 に送信されたトラフィックと同じ分だけ store-v2 にも送信されていました。

実行結果 store-v2 のログとの比較
    100 store-v1
100

検証結果を比較するため、以下の様に store-mirror.yamlfilters 行から下を全てコメントアウトして、デプロイします。

store-mirror.yaml
store-mirror.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: http-filter-mirroring
spec:
  parentRefs:
    - kind: Gateway
      name: external-https
  rules:
    - backendRefs:
        - name: store-v1
          port: 8080
#      filters:
#        - type: RequestMirror
#          requestMirror:
#            backendRef:
#              name: store-v2
#              port: 8080

その後、前述と同様に curl 実行件数とログ件数を比較しましたが、以下のような差が出ました。
当たり前と言えば当たり前ですが、 store-v2 へのトラフィックミラーリングを止めたため、ログ件数はゼロになっています。

実行結果 store-v1 のログとの比較
    100 store-v1
102
実行結果 store-v2 のログとの比較
    100 store-v1
0

クリーンアップ

今回作成した HTTPRoute リソースや、Gateway リソース等を削除しておきましょう。

# HTTPRoute リソース削除
kubectl delete httproute http-filter-mirroring
# Gateway リソース削除
kubectl delete gtw external-https
# デモアプリケーション削除
kubectl delete -f https://raw.githubusercontent.com/GoogleCloudPlatform/gke-networking-recipes/main/gateway/gke-gateway-controller/app/store.yaml

また、必要に応じてGKEクラスタ、グローバル外部IPアドレスリソース等も不要であれば削除しましょう。

その他の課題

トラフィックミラーリング(requestMirror)でバックエンドを初回設定すると設定に失敗する

トラフィックミラーリングですが、まっさらな状態で今回例示した設定を行うと、以下のようなメッセージが出力されて HTTPRoute リソースの設定がエラーになります。
原因は不明ですがおそらくまだ GKE Gateway controller は requestMirror に指定したバックエンドに対応するNEGリソースがない場合、新規作成せずエラー扱いにしてしまうためと考えられます。
今回のケースでは先に store-v2 を通常の backendRefs で設定してNEGリソースを作るよう誘導してあげないといけないようです。

エラーメッセージ
      message: 'error cause: no-error-isolation: Error GWCER103: Cannot find NEGs
        for Service Port default/store-v2/8080.'

今回のブログ記事作成では、トラフィック分割の検証を行ってからトラフィックミラーリングの検証を行うと問題無かったのですが、別日にトラフィイクミラーリングのみ検証したら前述のエラーにより難航したため苦労しました。

RedirectやRewrite機能には未対応

グローバル外部 HTTP(S) ロードバランサの機能としては実装されており、Gateway API としても存在自体はあるのですが、 GKE Gateway controller では以下のドキュメントにあるようなリクエストのRedirectやRewriteは使えないようでした。

https://gateway-api.sigs.k8s.io/v1alpha2/guides/http-redirect-rewrite/

調査しきれていないだけかも知れませんが、この機能もあると嬉しいので今後に期待です。

まとめ

前回のブログ記事作成から1週間経たずに新しい機能が追加されたので、 GKE Gateway controller は進歩が早いなという印象です。
今後も、新しい機能追加をウォッチしつつ、適宜検証してブログ化できればと思っております。

Discussion