🛠️

GKE Gateway Controller でサービスネットワーキング

2023/09/25に公開

7 月に職場が変わり従事しているプロダクトの下回りで使っている GKE と仲良くなりたいと思う、今日この頃です。Google 主催のハンズオンなどに参加する中で Kubernetes でサービスを外部公開する際の API として Gateway API なるものが知り、Ingress に変わって Gateway が使われるケースも増えてくるだろうと話を聞きました。


また、Gateway API でデプロイしたリソースを Google Cloud の世界に紐づける GKE Gateway Controller なるものがあり、2021 年 5 月に Preview されて、2022 年 11 月に GA されています。GA 以降も定期的に機能が追加されているようです。

今回はこのあたりをまとめようと思います。既出の内容も多いところですが、マニフェストのサンプルを備忘録的に残しておきたいので、GKE × Gateway API の参考になれば幸いです!

Overview

Gateway API と GKE Gateway Controller によるサービス外部公開の検証

  • Gateway API を利用した Cloud Load Balancing のプロビジョニングによるサービスの外部公開を実現することができました
  • Gateway API の中の HTTPRoute リソースで細かいトラフィック管理が可能でした
  • Gateway リソースと Namespace リソースで特定の設定をすることで Namespace をまたいだルーティングも可能でした

所感

ServiceNEG, HTTPRouteURL Map(Routing Rule) & Backend Service, GatewayCloud Load Balacing & Frontend という対応関係が存在する点や Kubernetes のリソースをデプロイ後にある程度時間が経ってからでないと Google Cloud 側にリソースが作成されない点を理解できるまでは使いづらかったですが、これらを理解した上だと特にルーティングの部分にフォーカスして細かく扱える点が良いと感じました。

あとは Namespace を超えてルーティング可能になるので、サービスごとの Namespace の切り分け・管理がよりしやすくなると感じました。 Google Cloud Next で発表があった GKE Enterprise EditionFleet に登録されたクラスタを Namespace 単位で横串にチームで管理できるようになったりと、 Namespace 単位での管理が流れとしてありそうです。

キーワード

Gateway API と GKE Gateway Controller

サービスを外部公開するための Gateway API を利用して定義した Kubernetes のリソースGKE Gateway Controller が Google Cloud の世界に紐づけてくれるようなイメージをしました。

公式ドキュメントには下記のように記載されています。

GKE Gateway Controller は、Google が Cloud Load Balancing に実装した Gateway API です。GKE Ingress Controller と同様に、Gateway Controller は Gateway API リソースの Kubernetes API を監視し、Cloud Load Balancing リソースを調整して、Gateway リソースで指定されたネットワーク動作を実装します。

Google Cloud 側から監視しているということで、下記の図では Google Cloud から Kubernetes リソースに矢印が向いているんですかね。

こちらの記事が基本情報をまとめてくれているので、本記事では定義方法と実際の挙動をまとめていきたいと思います。

サンプルとして Nginx をデプロイ

トラフィック分割やリダイレクトなどトラフィック管理やルーティングを試したいのでサンプルとして Nginx を GKE にデプロイします。GKE のプロビジョニングについてはこちらをご参考ください。

nginx
 |- gateway.yaml
 |- httproute.yaml
 |- httproute2.yaml
 |- nginx-v1.yaml
 |- nginx-v2.yaml
 |- nginx-v2-2.yaml
 |- v1
 |  |- Dockerfile
 |  |- default.conf
 |  |- index.html
 |- v2
 |  |- Dockerfile
 |  |- default.conf
 |  |- index.html

Nginx イメージの格納

2 つのバージョンの Nginx を作成します。 共通で /test/ で簡単な html を表示させ、バージョンで異なる内容を表示します。また、バージョン 2 のみ /test2/ で簡単な html を表示させます。

v1/Dockerfile
FROM 'nginx:latest'

RUN mkdir /usr/share/nginx/html/test
COPY ./index.html /usr/share/nginx/html/test
COPY ./default.conf /etc/nginx/conf.d

RUN service nginx start
v1/default.conf
server {
    listen 80 default_server;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location = /test {
        root   /usr/share/nginx/html;
    }
}
v1/index.html
v1/index.html
<h1>Hello World!</h1>
v2/Dockerfile
FROM 'nginx:latest'

RUN mkdir /usr/share/nginx/html/test
COPY ./index.html /usr/share/nginx/html/test
RUN mkdir /usr/share/nginx/html/test2
COPY ./index.html /usr/share/nginx/html/test2

COPY ./default.conf /etc/nginx/conf.d

RUN service nginx start

v2/default.conf
server {
    listen 80 default_server;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location = /test {
        root   /usr/share/nginx/html;
    }
    
    location = /test2 {
        root   /usr/share/nginx/html;
    }
}
v2/index.html
v2/index.html
<h1>Hello World! v2</h1>

Artifact Registry の作成から各種イメージの格納までを行います。

terminal
# Artifact Registry の作成
gcloud artifacts repositories create sample --location=asia-northeast1 --repository-format=docker

# Artifact Registry の認証
gcloud auth configure-docker asia-northeast1-docker.pkg.dev

# v1 のビルドおよび格納
cd nginx/v1
docker build -f Dockerfile -t asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v1 .
docker push asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v1

# v2 のビルドおよび格納
cd ../v2
docker build -f Dockerfile -t asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v2 .
docker push asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v2

Nginx のデプロイ

両バージョンともほぼ同一の内容となるので v1 のものだけ載せます。

nginx-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment-v1
  namespace: sample
  labels:
    apps: nginx-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-v1
  template:
    metadata:
      labels:
        app: nginx-v1
    spec:
      containers:
      - name: sample-nginx
        image: asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v1
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-v1
  namespace: sample
  annotations:
    cloud.google.com/app-protocols: '{"my-http-port":"HTTP"}'
    cloud.google.com/neg: '{"exposed_ports": {"80":{}}}'
spec:
  selector:
    app: nginx-v1
  type: ClusterIP
  ports:
  - name: my-http-port
    port: 80
    targetPort: 80
# deployment と service の作成
kubectl apply -f nginx-v1.yaml

# service の情報を取得
kubectl describe svc nginx-v1 -n sample

...
Events:
  Type    Reason  Age   From                   Message
  ----    ------  ----  ----                   -------
  Normal  ADD     47s   sc-gateway-controller  sample/nginx-v1
  Normal  UPDATE  47s   sc-gateway-controller  sample/nginx-v1
  Normal  Create  39s   neg-controller         Created NEG "k8s1-194efb7a-sample-nginx-v1-80-2693315c" for sample/nginx-v1-k8s1-194efb7a-sample-nginx-v1-80-2693315c-my-http-port/80-80-GCE_VM_IP_PORT-L7 in "asia-northeast1-a".
  Normal  Attach  36s   neg-controller         Attach 1 network endpoint(s) (NEG "k8s1-194efb7a-sample-nginx-v1-80-2693315c" in zone "asia-northeast1-a")

Service の作成とともに Google Cloud の世界に Network Endpoint Group を作成します。この NEG によって Kubernetes の世界と Google Cloud の世界がつながることできて、コンテナネイティブの負荷分散が実現できるようです。

こちらの Service では下記がブラウザに表示されます。(現時点ではブラウザからはアクセスできません)

v2 も同様にデプロイするとこんな感じになります。

これらを Gateway API を利用して外部に公開していきます。

GatewayClass

GatewayClasskind で Gateway を指定したマニフェストで記述します。gkeasm が Prefix であるもので Cloud Load Balancing の種別を指定することが可能です。(参考)

今回はグローバル外部アプリケーションロードバランサを利用したいので gke-l7-global-external-managed を選択します。

Gateway API を利用するためにはクラスターの更新が必要となります。

# クラスターの更新
gcloud container clusters update [クラスター名] \
    --gateway-api=standard \
    --location=[リージョン or ゾーン]
    
# 利用可能な GatewayClass を確認
kubectl get gatewayclass     

Gateway

Gateway では Cloud Load Bancing の名称や構成要素の 1 つであるフロントエンドを指定します。GatewayClass Name に上記の種別を記述します。

外部ロードバランサということで外部 IP が必要となります。 sample-ip という名前かつグローバルで作成します。

gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: sample
---
apiVersion:  gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: sample-gateway
  namespace: sample
spec:
  gatewayClassName: gke-l7-global-external-managed
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      kinds:
      - kind: HTTPRoute
  addresses:
  - type: NamedAddress
    value: sample-ip
# 外部 IP の予約
gcloud compute addresses create sample-ip --global --project=sample-project

# GKE の Config を取得
gcloud container clusters get-credentials sample-cluster --zone asia-northeast1 --project sample-project

# namespace と gateway をデプロイ
kubectl apply -f gateway.yaml

# gateway の情報を確認
kubectl describe gateway sample-gateway -n sample

...
Events:
  Type    Reason  Age                 From                   Message
  ----    ------  ----                ----                   -------
  Normal  ADD     2m5s                sc-gateway-controller  sample/sample-gateway
  Normal  UPDATE  53s (x3 over 2m5s)  sc-gateway-controller  sample/sample-gateway
  Normal  SYNC    20s (x2 over 53s)   sc-gateway-controller  SYNC on sample/sample-gateway was a success

describe の Events の結果で SYNC が正常に完了すると Cloud Load Balancing が作成されます。出力からも GKE Gateway Controller が仕事してくれている感じがしますね。

Cloud Load Balancing の名前は gkegwxxxx-[Namespace名]-[Gateway名]
-xxxx
のようになります。

フロントエンド

アタッチされている外部 IP も先ほど払い出したものと一致しています。

当然ですが、ルーティングルールは存在していません。該当の IP にアクセスすると fault filter abort と表示されるかと思います。

HTTPRoute

HTTPRoute ではルーティングルールとバックエンドサービスを追加していきます。HTTPRoute では特に HTTPRouteRule の種類が多く、細かいトラフィック管理ができそうです。

ここから 5 つのルーティングパターンを紹介します。

1. シンプルルーティング

一番オーソドックスなルーティングはこちらです。冒頭でも説明しましたが http://[外部IP]/test/ で指定した html が返ってくるようにしています。

httproute.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: nginx-httproute
  namespace: sample
spec:
  parentRefs:
  - kind: Gateway
    name: sample-gateway
    namespace: sample
  # hostnames:
  # - "*"
  rules:
  - backendRefs:
    - name: nginx-v1
      port: 80
# httproute のデプロイ
kubectl apply -f httproute.yaml

# httproute の情報を確認
kubectl describe httproute nginx-httproute -n sample

...
Events:
  Type    Reason  Age                   From                   Message
  ----    ------  ----                  ----                   -------
  Normal  ADD     4m12s                 sc-gateway-controller  sample/nginx-httproute
  Normal  SYNC    2m33s (x2 over 3m6s)  sc-gateway-controller  Bind of HTTPRoute "sample/nginx-httproute" to ParentRef {Group:       "gateway.networking.k8s.io",
 Kind:        "Gateway",
 Namespace:   "sample",
 Name:        "sample-gateway",
 SectionName: nil,
 Port:        nil} was a success

SYNC が正常に完了するとブラウザからアクセス可能になります。

ルーティングルール

コンソール画面のルーティングルールから url-maps を確認できます。

また、gcloud コマンドでの確認は下記になります。

# url-map の一覧を取得
gcloud compute url-maps list

# 該当の url-map の詳細を取得
gcloud compute url-maps describe [url-map 名]

バックエンド

コンソール画面のバックエンドには最初にデプロイした Service である nginx-v1 が紐づいていることが確認できます。実態としては Service 作成時に Google Cloud の世界に作成された NEG がバックエンドとして登録されていそうです。

2. パスマッチによるルーティング

次にパスによってルーティング先を変更してみます。 /test/ のルーティングは残しつつ、 /test2/Nginx-v2 にルーティングされるように設定します。

httproute.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: nginx-httproute
  namespace: sample
spec:
  parentRefs:
  - kind: Gateway
    name: sample-gateway
    namespace: sample
  # hostnames:
  # - "*"
  rules:
  - backendRefs:
    - name: nginx-v1
      port: 80
  - matches:
    - path:
        value: /test2/
    backendRefs:
    - name: nginx-v2
      port: 80
# httproute のデプロイ
kubectl apply -f httproute.yaml

ルーティングルール

コンソール画面から確認すると matchRules が増えています。紐づいているバックエンドサービスが Nginx-v2 になっています。

バックエンド

こちらも想定通りバックエンドサービスとして Nginx-v2 が追加されていて、NEG が紐づいています。

3. リダイレクト

リダイレクトも簡単に実装できます。/redirect//test/ にリダイレクトされるようにします。 httproute.yaml に修正します。

httproute.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: nginx-httproute
  namespace: sample
spec:
  parentRefs:
  - kind: Gateway
    name: sample-gateway
    namespace: sample
  # hostnames:
  # - "*"
  rules:
  - backendRefs:
    - name: nginx-v1
      port: 80
  - matches:
    - path:
        value: /redirect/
    filters:
      - type: RequestRedirect
        requestRedirect:
          path:
            type: ReplaceFullPath
            replaceFullPath: /test/
# httproute のデプロイ
kubectl apply -f httproute.yaml

http://[外部 IP]/redirect/ と入力すると http://[外部 IP]/test/ にリダイレクトされます。

ルーティングルール

コンソール画面から確認すると urlRedirect という項目が増えています。優先度を指定しないとデプロイした最新の routeRules が高くなっていくようです。

4. トラフィック分割

トラフィック分割も簡単に実装できます。/test/ にアクセスすると、 nginx-v1nginx-v2 に 1:1 の比率でルーティングされるように設定します。

httproute.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: nginx-httproute
  namespace: sample
spec:
  parentRefs:
  - kind: Gateway
    name: sample-gateway
    namespace: sample
  # hostnames:
  # - "*"
  rules:
  - matches:
    - path:
        value: /test/
    backendRefs:
    - name: nginx-v1
      port: 80
      weight: 50
    - name: nginx-v2
      port: 80
      weight: 50
# httproute のデプロイ
kubectl apply -f httproute.yaml

みてわかる通りですが、weight の部分でルーティングされる比率を制御しています。何回かリロードすると無印と v2 が大体交互に表示されるかと思います。

ルーティングルール

コンソール画面から確認すると matchRules の中に routeAction: weightBackendServicesweight が 50 ずつになっています。

5. Cross-Namespace ルーティング

Ingress では単一の Namespace へのルーティングだけだったのが、GatewayHTTPRoute では Namespace またぎのルーティングが可能になります。

新しく sample2 という Namespace を作成します。

Gateway に関する変更点としては、 shared-gateway-accesstrue にするのがポイントです。このように設定することで、metadata.labels.shared-gateway-accesstrue になっている Namespace のみ HTTPRouteGateway にアタッチに可能になります。

gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: sample
  labels:
    shared-gateway-access: "true"
---
apiVersion: v1
kind: Namespace
metadata:
  name: sample2
  labels:
    shared-gateway-access: "true"
---
apiVersion:  gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: sample-gateway
  namespace: sample
spec:
  gatewayClassName: gke-l7-global-external-managed
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            shared-gateway-access: "true"
  addresses:
  - type: NamedAddress
    value: sample-ip

こちらの sample2Nginx-v2 をデプロイします。

nginx-v2-2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment-v2
  namespace: sample2
  labels:
    apps: nginx-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-v2
  template:
    metadata:
      labels:
        app: nginx-v2
    spec:
      containers:
      - name: sample-nginx
        image: asia-northeast1-docker.pkg.dev/sample-project/sample/nginx:v2
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-v2
  namespace: sample2
  annotations:
    cloud.google.com/app-protocols: '{"my-http-port":"HTTP"}'
    cloud.google.com/neg: '{"exposed_ports": {"80":{}}}'
spec:
  selector:
    app: nginx-v2
  type: ClusterIP
  ports:
  - name: my-http-port
    port: 80
    targetPort: 80

トラフィック分割で利用した httproute.yaml はデプロイしたままで、 sample2Namespace にデプロイする httproute2.yaml を作成します。

httproute2.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: nginx-httproute
  namespace: sample2
spec:
  parentRefs:
  - kind: Gateway
    name: sample-gateway
    namespace: sample
  # hostnames:
  # - "*"
  rules:
  - matches:
    - path:
        value: /test2/
    backendRefs:
    - name: nginx-v2
      port: 80
# sample2 の Namespace を作成
# sample と sample2 に shared-gateway-accesss のラベルを付与
kubectl apply -f gateway.yaml

# sample2 に Service を作成
kubectl apply -f nginx-v2-2.yaml

# sampl2 に HTTPRoute を作成
kubectl apply -f httproute2.yaml

Namespace をまたいで /test2/sample2Nginx-v2 にアクセス可能となりました!

ルーティングルール

コンソール画面から確認すると、特に Namespace に関する情報はありませんでした。結局は NEG がどのように紐づいているかの話なので、 Google Cloud の世界には関係がないようです。

まとめ

GKE Gateway Controller を介して Gateway API によってサービスを外部公開することができました。

外部公開した際のルーティングは細かく実装することができました。

  • シンプルなルーティング
  • パスマッチによるルーティング
  • ルーティングにおけるトラフィック分割
  • Namespace をまたいだルーティング

Gateway API によって Namespace 単位でのサービス管理・公開がより促進されるように感じました。少しだけ GKE と仲良くなることができました。

参考記事

さいごに

AWS と Google Cloud で構築したデータ基盤の開発・運用に携わっているデータエンジニアです。5 年くらい携わっていて、この業務がきっかけで Google Cloud が好きになりました。

現在は React のフロントエンジニアとして修行中です。

X では Google Cloud 関連の情報を発信をしています。

https://twitter.com/pHaya72

Discussion