コンテンツにスキップ

【Envoy Gateway】Gateway Namespace Mode を試してみた

Envoy Gateway の Gateway Namespace Mode を使うと、Envoy Proxy などのデータプレーンのリソースを、Gateway と同じ Namespace へ配置できます。標準モードとの配置先の違いを整理し、v1.4.0 の設定例を実際に動かしてみます。

標準デプロイモードとの違い

Envoy Gateway の標準デプロイモードでは、Envoy Gateway コントローラーが Envoy Proxy に関連するリソース(Deployment, Service 等)を作成します。Gateway Namespace Modeとの違いはデータプレーンリソースの配置先で、両モードを比較すると次のとおりです。

モード データプレーンリソースの配置先
標準デプロイモード Envoy Gateway コントローラーと同じ Namespace(通常envoy-gateway-system)
Gateway Namespace Mode 各 Gateway リソースが定義された Namespace

データプレーンリソースを Gateway ごとの Namespace に配置する設計により、Gateway Namespace Modeはリソース分離とマルチテナンシーの強化を実現します。

留意事項

本記事執筆時点(v1.4.0)の Gateway Namespace Mode はアルファ版の機能で、v1.4 の公式ドキュメントはベータ版になるまで実稼働環境で使わないよう推奨しています。また、v1.4.0 では Merged Gateways との併用がサポートされていません。機能の変更や不具合の報告は、公式 GitHub issuesで確認できます。

Gateway Namespace Mode の概要

Gateway Namespace Mode の主要な特徴は、次の 2 点です。

データプレーンリソースの分離

Envoy Proxy の Deployment, Service, ServiceAccount といったデータプレーンリソースが、Gateway リソースと同一の Namespace に作成されます。同一 Namespace への配置により、テナントやチーム単位でのリソース分離が明確になります。

認証方式の変更

標準モードではインフラとコントロールプレーン間で mTLS (相互 TLS 認証) を使用しますが、Gateway Namespace Mode ではサーバーサイド TLS および JWT (JSON Web Token) 検証へと移行しています。変更点の詳細は次のとおりです。

  • 認証に使うトークン: projected service account JWT token(有効期間が短く対象オーディエンスを限定したトークンを、Pod へ自動的にマウント)
  • JWT 検証の役割: 認可されたプロキシのみが xDS サーバーに接続できることの保証
  • Gateway の Namespace で実行される Pod の証明書: CA 証明書のみが利用可能で、クライアント証明書はマウントしない
  • Envoy Proxy によるサーバー証明書の検証: 引き続き CA 証明書を使用

設定手順

Gateway Namespace Mode を有効化するには、Envoy Gateway の Helm チャートの値でprovider.kubernetes.deploy.typeフィールドをGatewayNamespaceに設定します。

1
2
3
4
5
6
7
config:
  envoyGateway:
    provider:
      type: Kubernetes
      kubernetes:
        deploy:
          type: GatewayNamespace

次のようにhelm installの--setで同じ値を指定しても、Gateway Namespace Mode を有効にしてインストールできます。

1
2
3
4
helm install \
  --set config.envoyGateway.provider.kubernetes.deploy.type=GatewayNamespace \
  eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.4.0 -n envoy-gateway-system --create-namespace

RBAC 設定

Gateway Namespace Mode では Envoy Gateway が異なる Namespace 間でリソースを作成・管理するため、追加の RBAC (Role-Based Access Control) 権限が必要です。Gateway Namespace Mode を有効にしてインストールする際、Helm チャートが RBAC リソース (ClusterRole, ClusterRoleBinding) を自動的に作成します。

例えば、Helm チャートは次のような ClusterRole を作成し、ServiceAccount, Service, Deployment 等のリソースに対するcreate, get, deleteなどの権限を付与します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gateway-helm-cluster-infra-manager
rules:
  - apiGroups: [""]
    resources: ["serviceaccounts", "services", "configmaps"]
    verbs: ["create", "get", "delete", "deletecollection", "patch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "daemonsets"]
    verbs: ["create", "get", "delete", "deletecollection", "patch"]
  - apiGroups: ["autoscaling", "policy"]
    resources: ["horizontalpodautoscalers", "poddisruptionbudgets"]
    verbs: ["create", "get", "delete", "deletecollection", "patch"]
  - apiGroups: ["authentication.k8s.io"]
    resources: ["tokenreviews"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gateway-helm-cluster-infra-manager
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: "gateway-helm-cluster-infra-manager"
subjects:
  - kind: ServiceAccount
    name: "envoy-gateway"
    namespace: "envoy-gateway-system"

監視対象 Namespace の絞り込み

EnvoyGateway.provider.kubernetes.watch.namespacesまたはEnvoyGateway.provider.kubernetes.watch.namespaceSelectorを指定すると、特定の Namespace のみを監視対象にできます。Gateway Namespace Mode と併用すると、Envoy Gateway は指定した Namespace の Gateway API リソースだけを監視し、インフラ管理に必要な Role も指定した Namespace へ自動作成します。

公式の例を試す

公式ドキュメントの例を参考に、Gateway Namespace Mode の動作を検証します。以下では、team-aとteam-bという 2 つの異なる Namespace に Gateway をデプロイします。

テスト用 Namespace の作成

まず、テスト用の Namespace を作成します。

kubectl create namespace team-a
kubectl create namespace team-b

サンプルマニフェストの適用

次に、公式ドキュメントのサンプルマニフェストを適用し、team-aおよびteam-bの各 Namespace にバックエンドの Deployment、Gateway、HTTPRoute を作成します。

kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/refs/heads/release/v1.4/examples/kubernetes/gateway-namespace-mode.yaml

リソースの確認

各リソースが正しくデプロイされたかを確認します。

Gateway の確認

1
2
3
4
5
6
7
8
9
kubectl get gateways -n team-a
# 出力例:
# NAME        CLASS   ADDRESS        PROGRAMMED   AGE
# gateway-a   eg      172.18.0.200   True         67s

kubectl get gateways -n team-b
# 出力例:
# NAME        CLASS   ADDRESS        PROGRAMMED   AGE
# gateway-b   eg      172.18.0.201   True         67s

PROGRAMMEDがTrueであることを確認します。

HTTPRoute の確認

1
2
3
4
5
6
7
8
9
kubectl get httproute -n team-a
# 出力例:
# NAME           HOSTNAMES            AGE
# team-a-route   ["www.team-a.com"]   67s

kubectl get httproute -n team-b
# 出力例:
# NAME           HOSTNAMES            AGE
# team-b-route   ["www.team-b.com"]   67s

Pod の確認

Gateway Namespace Mode の特徴として、Envoy Proxy の Pod が各 Gateway の Namespace (team-a, team-b) に作成されます。

kubectl get pods -n team-a
# 出力例:
# NAME                                              READY   STATUS    RESTARTS   AGE
# envoy-team-a-gateway-a-b65c6264-d56f5d989-6dv5s   2/2     Running   0          65s
# team-a-backend-6f786fb76f-nx26p                   1/1     Running   0          65s

kubectl get pods -n team-b
# 出力例:
# NAME                                               READY   STATUS    RESTARTS   AGE
# envoy-team-b-gateway-b-0ac91f5a-74f445884f-95pl8   2/2     Running   0          87s
# team-b-backend-966b5f47c-zxngl                     1/1     Running   0          87s

envoy-team-a-gateway-a-やenvoy-team-b-gateway-b-で始まる名称の Pod が各 Namespace に存在し、READY 2/2であることを確認します。

Service の確認

同様に、Envoy Proxy の Service も各 Namespace に作成されます。

kubectl get services -n team-a
# 出力例:
# NAME                              TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
# envoy-team-a-gateway-a-b65c6264   LoadBalancer   10.96.191.198   172.18.0.200   8080:30999/TCP   3m2s
# team-a-backend                    ClusterIP      10.96.92.226    <none>         3000/TCP         3m2s

kubectl get services -n team-b
# 出力例:
# NAME                              TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
# envoy-team-b-gateway-b-0ac91f5a   LoadBalancer   10.96.144.13   172.18.0.201   8081:31683/TCP   3m43s
# team-b-backend                    ClusterIP      10.96.26.162   <none>         3000/TCP         3m43s

動作テスト

最後に、実際にリクエストを送信してルーティングの動作を確認します。

外部 IP の取得

まず、各 Gateway Service の外部 IP アドレスを取得します。

export GATEWAY_HOST_A=$(kubectl get gateway/gateway-a -n team-a -o jsonpath='{.status.addresses[0].value}')
export GATEWAY_HOST_B=$(kubectl get gateway/gateway-b -n team-b -o jsonpath='{.status.addresses[0].value}')

team-a へのリクエスト

team-aの Gateway (ポート8080) を経由して、www.team-a.comへのリクエストを送信します。

curl --header "Host: www.team-a.com" http://$GATEWAY_HOST_A:8080/example

次のようなレスポンスが返ります。

{
  "path": "/example",
  "host": "www.team-a.com",
  "method": "GET",
  "proto": "HTTP/1.1",
  "headers": {
    "Accept": ["*/*"],
    "User-Agent": ["curl/8.7.1"],
    "X-Envoy-External-Address": ["172.18.0.3"],
    "X-Forwarded-For": ["172.18.0.3"],
    "X-Forwarded-Proto": ["http"],
    "X-Request-Id": ["52f08a5c-7e07-43b7-bd23-44693c60fc0c"]
  },
  "namespace": "team-a",
  "ingress": "",
  "service": "",
  "pod": "team-a-backend-6f786fb76f-nx26p"
}

レスポンス内の"namespace": "team-a"と、team-a-backend-で始まる"pod"の値から、team-aのバックエンドへのルーティングを確認できます。

team-b へのリクエスト

team-bの Gateway (ポート8081) を経由して、www.team-b.comへのリクエストを送信します。

curl --header "Host: www.team-b.com" http://$GATEWAY_HOST_B:8081/example

team-bでも、次のようなレスポンスが返ります。

{
  "path": "/example",
  "host": "www.team-b.com",
  "method": "GET",
  "proto": "HTTP/1.1",
  "headers": {
    "Accept": ["*/*"],
    "User-Agent": ["curl/8.7.1"],
    "X-Envoy-External-Address": ["172.18.0.3"],
    "X-Forwarded-For": ["172.18.0.3"],
    "X-Forwarded-Proto": ["http"],
    "X-Request-Id": ["62a06bd7-4754-475b-854a-dca3fc159e93"]
  },
  "namespace": "team-b",
  "ingress": "",
  "service": "",
  "pod": "team-b-backend-966b5f47c-d6jwj"
}

同様に、"namespace": "team-b"からteam-bのバックエンドへのルーティングを確認できます。

以上の手順により、Gateway Namespace Mode が正しく機能し、各 Namespace の Gateway がそれぞれのデータプレーンリソースを保持して独立して動作していることを確認できました。

ClusterIP での動作確認

Gateway Namespace Mode では、各 Gateway の Namespace に Envoy Proxy の Service を作成しますが、ClusterIP タイプの Service でも動作します。デフォルトの Service タイプは、外部 IP アドレスを持つ LoadBalancer です。ClusterIP にすると外部 IP を持たない Service となり、通信は Kubernetes クラスタ内に閉じます。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: example-envoy-proxy
  namespace: team-a
spec:
  provider:
    kubernetes:
      envoyService:
        name: my-gateway
        type: ClusterIP # 公開しない場合は ClusterIP を指定
    type: Kubernetes
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy # GatewayClassにEnvoyProxyを紐付ける
    name: example-envoy-proxy
    namespace: team-a
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: team-a-backend
  namespace: team-a
---
apiVersion: v1
kind: Service
metadata:
  name: team-a-backend
  namespace: team-a
  labels:
    app: team-a-backend
    service: team-a-backend
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: team-a-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: team-a-backend
  namespace: team-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: team-a-backend
      version: v1
  template:
    metadata:
      labels:
        app: team-a-backend
        version: v1
    spec:
      serviceAccountName: team-a-backend
      containers:
        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          imagePullPolicy: IfNotPresent
          name: team-a-backend
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: team-b-backend
  namespace: team-b
---
apiVersion: v1
kind: Service
metadata:
  name: team-b-backend
  namespace: team-b
  labels:
    app: team-b-backend
    service: team-b-backend
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: team-b-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: team-b-backend
  namespace: team-b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: team-b-backend
      version: v1
  template:
    metadata:
      labels:
        app: team-b-backend
        version: v1
    spec:
      serviceAccountName: team-b-backend
      containers:
        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          imagePullPolicy: IfNotPresent
          name: team-b-backend
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway-a
  namespace: team-a
spec:
  gatewayClassName: eg
  listeners:
    - allowedRoutes:
        namespaces:
          from: Same
      name: http
      port: 8080
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway-b
  namespace: team-b
spec:
  gatewayClassName: eg
  listeners:
    - allowedRoutes:
        namespaces:
          from: Same
      name: http
      port: 8081
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: team-a-route
  namespace: team-a
spec:
  parentRefs:
    - name: gateway-a
  hostnames:
    - "www.team-a.com"
  rules:
    - backendRefs:
        - group: ""
          kind: Service
          name: team-a-backend
          port: 3000
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /example
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: team-b-route
  namespace: team-b
spec:
  parentRefs:
    - name: gateway-b
  hostnames:
    - "www.team-b.com"
  rules:
    - backendRefs:
        - group: ""
          kind: Service
          name: team-b-backend
          port: 3000
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /example

先ほど検証で使用した公式のマニフェストを元に、新しく作成した EnvoyProxy リソースを GatewayClass に紐付けたのが上記のマニフェストです。この EnvoyProxy リソースを使うと、Envoy Gateway は Envoy Proxy の Service を ClusterIP タイプで作成します。複数の Namespace で 1 つの EnvoyProxy リソースを共有しましたが、Namespace ごとに EnvoyProxy リソースを作成する構成も取れます。

通信の確認

各 Gateway へリクエストを送り、疎通を確認します。

1
2
3
4
kubectl run curlpod --rm -i -t --restart=Never --image=mirror.gcr.io/curlimages/curl:latest -- sh

curl my-gateway.team-a.svc.cluster.local:8080/example --header "Host: www.team-a.com"
curl my-gateway.team-b.svc.cluster.local:8081/example --header "Host: www.team-b.com"

上記のコマンドを実行すると公式例と同様のレスポンスを確認できるため、実行結果の記述は省略します。

結論

Envoy Gateway の Gateway Namespace Mode を利用すると、データプレーンリソースを Gateway ごとに分離でき、マルチテナンシー環境における分離性と管理性が向上します。認証メカニズムも JWT ベースに移行し、セキュリティ面も考慮されています。

v1.4.0 時点ではアルファ版ですが、今後の発展を期待できる機能の 1 つです。実稼働環境で利用する際は慎重に検討し、公式ドキュメントや GitHub issue で最新情報を確認することを推奨します。

参照資料