Managed ASM の新機能 Service Mesh Cloud Gateway とは

以下のリリースノートのとおり、Managed Anthos Service Mesh (以下、Managed ASM) に Service Mesh Cloud Gateway という新しい機能がリリースされました。
早速試してみたいと思います。

cloud.google.com

概要

従来、Istio Ingress Gateway を Cloud Load Balancing とつなぐには、Ingress Gateway の Service リソースに cloud.google.com/neg: '{"ingress": true}' などのアノテーションを付与することで NEG の作成と同時に Ingress Controller に Cloud Load Balancing の各種リソースを自動生成させる(あるいは Standalone NEG を作成し手動で Backend Service に指定する)ことで、Backend Service と NEG を接続する必要がありました。
今回リリースされた Service Mesh Cloud Gateway では、昨年 GA となった Kubernetes Gateway API を使って、Cloud Load Balancing と Anthos Service Mesh Ingress Gateway をひとまとめで作成できるようになりました。
現状はプレビューリリースとなっており、以下のように複数の制約があることに注意しましょう。

  • Rapid Channel の Managed ASM を利用する v1.24 以降の GKE クラスタのみで利用可能
  • Classic External L7 LB のみ作成可能
  • Autopilot では利用不可 など

公式ドキュメントは以下にあります。

cloud.google.com

Service Mesh Cloud Gateway を展開してみる

ここからは、こちら の公式ドキュメントの手順を追って Service Mesh Cloud Gateway を展開してゆきたいと思います。

まずは以下のように In-cluster Control Plane の GKE クラスタを用意しました。

$ gcloud container clusters describe sample-cluster --region=us-central1
currentMasterVersion: 1.25.5-gke.1500
currentNodeVersion: 1.25.5-gke.1500
location: us-central1
locations:
- us-central1-a
- us-central1-f
- us-central1-c
name: sample-cluster
networkConfig:
  gatewayApiConfig:
    channel: CHANNEL_STANDARD
  ...
releaseChannel:
  channel: RAPID
resourceLabels:
  mesh_id: proj-***********
status: RUNNING
zone: us-central1
...

次に、GatewayClass リソースを展開します。
GatewayClass リソースは、Kubernetes Gateway API を使ってロードバランサを作成するためのテンプレートを定義するクラスタスコープのリソースです。
今回は、Service Mesh Cloud Gateway がサポートしている asm-l7-gxlb の GatewayClassを利用します(Service Mesh Cloud Gateway としては現状 Classic External L7 LB のみのサポートとなっていますが、こちらの一覧 に記載のとおり、GKE で利用可能な GatewayClass としては他の Cloud Load Balancing のためのテンプレートも用意されています)。

GatewayClass リソースの主な役割は、この後に展開する Gateway リソースをどの GKE Gateway Controller に制御させるかを決定することにあります。
GatewayClass は Gateway を作成する際の実装を抽象化する役割を担うため、Gateway は自身の利用する GatewayClass を指定し、GatewayClass は自身のテンプレートに沿ったインスタンス化を行ってくれる Gateway Controller を指定し、指定されたコントローラが Ingress Gateway や Cloud Load Balancing のセットアップを行う、という仕組みになっています。

ちなみに、コントローラの実装も進化しており、GKE Gateway Controller は(以前から提供されていた GKE Ingress Controller とは異なり)Google がホストしているため、クラスタやプロジェクトに依存せず利用できるようになっています。

$ cat l7-gateway-class.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: GatewayClass
metadata:
  name: asm-l7-gxlb
spec:
  controllerName: mesh.cloud.google.com/gateway

$ kubectl apply -f l7-gateway-class.yaml
gatewayclass.gateway.networking.k8s.io/asm-l7-gxlb created

$ kubectl get gatewayclasses.gateway.networking.k8s.io
NAME                             CONTROLLER                      ACCEPTED   AGE
asm-l7-gxlb                      mesh.cloud.google.com/gateway   Unknown    17h
gke-l7-global-external-managed   networking.gke.io/gateway       True       18h
gke-l7-gxlb                      networking.gke.io/gateway       True       18h
gke-l7-rilb                      networking.gke.io/gateway       True       18h
istio                            istio.io/gateway-controller     True       13h

ACCEPTED が Unknown になっているのがちょっと気になりますが先に進みましょう。※

準備ができたので Gateway リソースを展開していきます。

$ kubectl get namespace istio-ingress
NAME            STATUS   AGE
istio-ingress   Active   17h

$ cat gateway.yaml
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All

$ kubectl apply -f gateway.yaml
gateway.gateway.networking.k8s.io/servicemesh-cloud-gw created

$ kubectl get gateways.gateway.networking.k8s.io -n istio-ingress
NAME                                CLASS         ADDRESS                                                                READY   AGE
asm-gw-gke-servicemesh-cloud-gw     gke-l7-gxlb   34.111.216.187                                                         True    17h
asm-gw-istio-servicemesh-cloud-gw   istio         asm-gw-istio-servicemesh-cloud-gw.istio-ingress.svc.cluster.local:80   True    17h
servicemesh-cloud-gw                asm-l7-gxlb                                                                                  17h

Gateway リソースが展開されると、Gateway Controller は asm-gw-istio-servicemesh-cloud-gw という名前で Ingress Gateway の Deployment を展開します。
加えて、この Ingress Gateway の NEG をバックエンドに設定した External L7 LB を自動作成します。
具体的には servicemesh-cloud-gw(GatewayClass: asm-l7-gxlb)が asm-gw-gke-servicemesh-cloud-gw(GatewayClass: gke-l7-gxlb)と asm-gw-istio-servicemesh-cloud-gw(GatewayClass: istio)を作成することで、asm-gw-gke-servicemesh-cloud-gw に基づいて L7 LB を作成し、asm-gw-istio-servicemesh-cloud-gw に基づいて Ingress Gateway を作成しているようです。

最後のコマンド kubectl get gateways.gateway.networking.k8s.io ... の実行結果の Gateway asm-gw-gke-servicemesh-cloud-gw の ADDRESS に表示されているのが、最終的に LB に付与された外部 IP アドレスですね。

では Service Mesh Cloud Gateway が展開できたので、リクエストを処理するサンプルアプリケーションも展開したいと思います。
手順の詳細は こちら の公式ドキュメントに載っていますので併せてご確認ください。

$ kubectl label namespace default istio-injection=enabled istio.io/rev- --overwrite
label "istio.io/rev" not found.
namespace/default labeled

$ cat whereami.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whereami-v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: whereami-v1
  template:
    metadata:
      labels:
        app: whereami-v1
    spec:
      containers:
      - name: whereami
        image: gcr.io/google-samples/whereami:v1.2.19
        ports:
          - containerPort: 8080
        env:
        - name: METADATA
          value: "whereami-v1"
---
apiVersion: v1
kind: Service
metadata:
  name: whereami-v1
spec:
  selector:
    app: whereami-v1
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whereami-v2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: whereami-v2
  template:
    metadata:
      labels:
        app: whereami-v2
    spec:
      containers:
      - name: whereami
        image: gcr.io/google-samples/whereami:v1.2.19
        ports:
          - containerPort: 8080
        env:
        - name: METADATA
          value: "whereami-v2"
---
apiVersion: v1
kind: Service
metadata:
  name: whereami-v2
spec:
  selector:
    app: whereami-v2
  ports:
  - port: 8080
    targetPort: 8080

$ kubectl apply -f whereami.yaml
deployment.apps/whereami-v1 created
service/whereami-v1 created
deployment.apps/whereami-v2 created
service/whereami-v2 created

$ kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
whereami-v1-768bf6449c-c87sz   2/2     Running   0          18h
whereami-v1-768bf6449c-gql2x   2/2     Running   0          18h
whereami-v2-7cf74dd8fc-bnbf5   2/2     Running   0          18h
whereami-v2-7cf74dd8fc-fvg74   2/2     Running   0          18h

$ cat http-route.yaml
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: where-route
spec:
 parentRefs:
 - kind: Gateway
   name: servicemesh-cloud-gw
   namespace: istio-ingress
 hostnames:
 - "where.example.com"
 rules:
 - matches:
   - headers:
     - name: version
       value: v2
   backendRefs:
   - name: whereami-v2
     port: 8080
 - backendRefs:
   - name: whereami-v1
     port: 8080

$ kubectl apply -f http-route.yaml
httproute.gateway.networking.k8s.io/where-route created

$ VIP=$(kubectl get gateways.gateway.networking.k8s.io asm-gw-gke-servicemesh-cloud-gw -o=jsonpath="{.status.addresses[0].value}" -n istio-ingress)

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-c87sz",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:10:08",
  "zone": "us-central1-f"
}

$ curl ${VIP} -H "host: where.example.com" -H "version: v2"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v2",
  "pod_name": "whereami-v2-7cf74dd8fc-fvg74",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:10:19",
  "zone": "us-central1-c"
}

上記ではまず、v1 と v2 の 2 つの API を提供するサンプルアプリケーション whereami を展開しました。
その後、先ほどクラスタに展開した Gateway servicemesh-cloud-gw を参照する HTTPRoute リソースを展開しました。
展開した HTTPRoute リソースは、ヘッダの有無でルーティング先を変える HTTPRouteMatch のルールを持っており、これに従ってそれぞれの API にアクセスできることが curl によるテストの結果からわかります。

なお、HTTPRoute は Istio の VirtualService に似ていますが、VirtualService がすべてのプロトコルをひとつのリソース内で構成していたのに対し、Gateway API では HTTPRouteTCPRoute のようにプロトコル毎に別々のリソースを利用する必要があります。

※ GatewayClass のステータスに関する補足

今回展開した GatewayClass asm-l7-gxlb の ACCEPTED のステータスは、展開から一定時間が経過した後も Unknown のままとなっていました。
Gateway API の Status を定義している GEP-1364 をざっと見たかぎりでは、ACCEPTED はコントローラによるバリデーションを通じて対象のリソースが正常に受け入れ可能と判定されると True になる、という定義のようです。
が、この GEP-1364 が取り込まれた Gateway API v0.6.0 は昨年末にリリースされたばかりという状況であるため、Service Mesh Cloud Gateway としては未対応という可能性はありそうです。

Service Mesh Cloud Gateway 特有の注意点

さて、ここまではほぼ公式ドキュメントに沿った流れでしたが、以降では Service Mesh Cloud Gateway の設定を色々と試していくなかで得られた知見をひとつご紹介したいと思います。

Kubernetes Gateway API のいち実装である Service Mesh Cloud Gateway は、基本的にはオープンソースGateway API の仕様に沿った挙動となっています。
が、現状の Service Mesh Cloud Gateway には、Gateway API の仕様に準拠していない挙動が一部含まれており、公式ドキュメントでは以下のように記載されています。

Route matching logic does not follow Gateway API specifications and matches in the order of the HTTPRoute instead.
Configure external HTTP(S) Load Balancing for managed Anthos Service Mesh  |  Google Cloud

直訳すると「ルートマッチングロジックは Gateway API の仕様に従わず、代わりに HTTPRoute の順序でマッチングする」という感じになるかと思いますが、この但し書きの意味を実際の挙動から確認してみましょう。

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-c87sz",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
kind: Gateway
  "timestamp": "2023-01-22T06:47:02",
  "zone": "us-central1-f"
}

$ cat gateway.yaml                                                                             
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-gql2x",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:47:08",
  "zone": "us-central1-a"
}

$ vim gateway.yaml

$ cat gateway.yaml 
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "where.example.com"
    allowedRoutes:
      namespaces:
        from: All

$ kubectl apply -f gateway.yaml
gateway.gateway.networking.k8s.io/servicemesh-cloud-gw configured

$ curl ${VIP} -H "host: where.example.com"
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>502 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered a temporary error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>

先ほどの Gateway servicemesh-cloud-gw の Listener に hostname を追加したところ、サンプルアプリケーションにルーティングされなくなってしまいました。
Listener にも hostname が付与されている場合の挙動を Gateway API ではどのように定義しているのか、HTTPRoute のリファレンスを確認してみましょう。

A Listener with test.example.com as the hostname matches HTTPRoutes that have either not specified any hostnames, or have specified at least one of test.example.com or *.example.com.
API specification - Kubernetes Gateway API

Gateway が受けたリクエストを処理する HTTPRoute の候補は複数存在する可能性があるため、どの Gateway が受けたリクエストをどの HTTPRoute に従ってルーティングするかを判断する必要がある、という事がわかります。
リクエストの HTTP ホストヘッダは Gateway に定義された Listener の hostname ないし HTTPRoute の hostname と照合されますが、Listener と HTTPRoute の両方に hostname が定義されている場合は、両方の指定したホスト名の条件に合致した HTTPRoute にリクエストを処理させる、という仕様になっているようです。
そして、仕様どおりであれば、Gateway の Listener に HTTPRoute と同じホスト名 'where.example.com' を追加しても Listener と HTTPRoute のマッチングには問題ないはずです(Google Cloud の公式ドキュメントでは、この Listener と HTTPRoute を結びつけるロジックのことをルートマッチングロジックと言っているようですね)。 しかし、ワイルドカードを含むいくつかのパターンを試した限りでは、Service Mesh Cloud Gateway の Listener に hostname を追加すると HTTPRoute とのマッチングが行えなくなっていました。

仕様と挙動の比較からの推測にはなりますが、「ルートマッチングロジックは Gateway API の仕様に従わず、代わりに HTTPRoute の順序でマッチングする」という説明の意味は、「現状の Service Mesh Cloud Gateway は、HTTPRoute の hostname にのみに従ってリクエストを処理する HTTPRoute を選択するため、Listener にホスト名を指定すると期待した動作にならない(Gateway API の仕様とは異なる挙動になる)」ということのようです。

実際の利用シーンでは Gateway の Listener にワイルドカードを使ったホスト名を指定しておき、HTTPRoute で特定のホスト名ごとに詳細な(例えば API バージョン毎の)ルーティングを行う、といった使い方も想定されるかと思いますが、現状は制約があると考えておくと良いでしょう。
なお、公式ドキュメントに記載のとおり、この挙動はいずれ Gateway API の仕様に沿うよう修正される見込みとのことなので、 Service Mesh Cloud Gateway が GA されるまでには Gateway API の仕様に準拠するのではないかと期待されます。

まとめ

Managed ASM の新機能 Service Mesh Cloud Gateway を試しました。
現状はプレビューゆえに複数の制約があり、利用シーンは限られていますが、今後より簡単に Istio メッシュと Google Cloud の橋渡し部分を設定してゆくことができるようになってゆきそうです。
ルートマッチングロジックの挙動については、あくまでプレビューリリースにおける一時的なものなので将来的には気にする必要がなくなるかと思いますが、私個人にとっては検証の過程で Service Mesh Cloud Gateway の実装や Gateway API の仕様の理解を深める良い機会になりました。