はじめに
こんにちは、技術本部 データサイエンス部 MLOpsブロックの鹿山(@Ash_Kayamin)です。
みなさんは2021年4月にGCPから「GKE Gateway コントローラによる Kubernetes ネットワーキングの進化」という記事が投稿されたのを覚えていますでしょうか。 cloud.google.com
この記事は、Kubernetesコミュニティが発表したKubernetes Gateway APIに対し、そのGKE(Google Kubernetes Engine)版実装であるGKE Gateway Controllerのリリースをアナウンスするものでした。
それから半年が経ち、本番導入の可能性を模索するためにKubernetes Gateway APIとGKE Gateway Controllerを調査、動作検証しました。本記事では、Kubernetes Gateway APIの概要と、APIで定義されるトラフィックのルーティングがGKE Gateway ControllerによってどのようにGCP上で実現されるのかを、動作検証の流れに沿って解説します。
なお、2021年11月時点で、Kubernetes Gateway APIの最新バージョンはv1alpha2
、GKE Gateway ControllerがサポートするGateway APIのバージョンはv1alpha1
であり、今後仕様が大きく変わる可能性がある点にご注意ください。
目次
Kubernetes Gateway APIの開発背景と特徴
よりスムーズに理解していただくために、Kubernetes Gateway APIが作成された背景から順にご紹介します。
Kubernetes Gateway APIが開発された背景
Gateway API(Kubernetes Gateway API)はIngress API(Kubernetes Ingress API)の課題を解消するために開発されました1。
そのIngress APIは、Kubernetesクラスタ外部からクラスタ内Service
(Kubernetes Service)に対し、アプリケーション層でHTTPやHTTPSを用いたルーティングを制御するAPIです。多数のプロバイダーでIngress APIの仕様に則ったIngress Controllerが実装されています。また、Ingress Controllerの実装によっては負荷分散やSSL終端といった機能も提供します。MLOpsブロックでもGKEでコンテナネイティブな負荷分散を利用するために、GCPが提供するIngress ControllerであるGLBCを利用しています。GLBCはIngress APIを通して、GCLB(Google Cloud Load Balancing)を用いたルーティングの設定が可能です。
実は、Ingress APIでは非常にシンプルな機能を実現するための仕様しか定義されていません。そのため、Ingress Controllerのプロバイダー、Ingress Controllerの利用者それぞれに以下の負担が発生していました。
- Ingress APIでは定義されていないトラフィックの荷重ルーティング等の機能を追加するには、プロバイダーは
Ingress
のManifestに独自のannotationを定義する必要がある - 開発者はプロバイダー毎にannotationが大きく異なるManifestを書かなくてはならない
例えば、各Ingress Controller毎にannotationへ定義可能な設定項目数を比較すると以下のように大きな差があります。
- GLBC:6個
- AWS Load Balancer Controller:40個
- NGINX Ingress Controller:110個
これは、対応している機能や、設定項目の表現方法(annotationのみを使うのか、annotationとCR(Custom Resource)を組み合わせるのか等)が異なるため、結果としてannotationの数に大きな差が生じています。
例えば、L7外部ロードバランサーを定義して、ロードバランサーをSSL終端とし、バックエンドのサービスへのヘルスチェックを設定することを考えてみます。
AWS Load Balancer Controllerを用いる場合のManifestは以下のように定義します。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: zozo-techblog annotations: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:xxxx alb.ingress.kubernetes.io/healthcheck-protocol: HTTP alb.ingress.kubernetes.io/healthcheck-port: '80' alb.ingress.kubernetes.io/healthcheck-path: /health spec: rules: - http: paths: - path: /* backend: serviceName: zozo-techblog servicePort: 80
一方、GLBCを用いる場合のManifestは以下のように定義します。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: zozo-techblog-ingress annotations: kubernetes.io/ingress.allow-http: "false" ingress.gcp.kubernetes.io/pre-shared-cert: "api-cert" spec: defaultBackend: service: name: zozo-techblog-service port: number: 80 --- apiVersion: v1 kind: Service metadata: name: zozo-techblog-service annotations: cloud.google.com/neg: '{"ingress": true}' # ref. https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing # BackendConfigを用いてヘルスチェック等をサービス毎にカスタマイズする cloud.google.com/backend-config: '{"default": "zozo-techblog-backendconfig"}' spec: selector: app: zozo-techblog-pod ports: - port: 80 protocol: TCP targetPort: 8080 --- apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: zozo-techblog-backendconfig spec: healthCheck: checkIntervalSec: 15 type: HTTP port: 8080 requestPath: /health
annotationが異なるのはもちろんですが、ヘルスチェックの指定方法が大きく異なっていることが分かります。AWS Load Balancer ControllerではIngress
のannotationにヘルスチェックの定義を記載します。一方、GLBCを用いる場合はBackendConfig
というCRにヘルスチェックの定義を記載します。そこでは、Ingress
で指定するService
のannotationでBackendConfig
を指定する必要があります。
また、L7ロードバランサーの機能への対応状況も大きく差があります。例えば、AWS Load Balancer ControllerではService
毎に割合を指定することでトラフィックの細かな分割ができます。一方、GLBCではトラフィックの分割割合の指定はできません。GLBCで設定をするL7ロードバランサーGCLBにはトラフィックの分割割合を指定する機能は存在します。しかしながら、現状GLBCにはGCLBでのトラフィック分割割合を設定する機能は実装されていません。L7ロードバランサーを構成するなら当然利用できるはずだと思う機能も、現状では利用できるかどうかはプロバイダー次第となってしまっています。
このように、Ingress Controller毎に対応している機能や設定項目の表現方法が大きく異なります。そのため、開発者が普段とは異なるIngress Controllerを利用する際には、Manifestを書くことに苦労します。
Gateway APIは、この問題を解消するために開発されました。Gateway APIはL4/L7ロードバランサーで実現可能なルーティングをできる限り共通の仕様で実現できるように配慮しています。
また、Ingress APIでは、L7でのService
へのルーティングの定義を1つのリソースで定義していました。一方、Gateway APIではルーティングの定義を責務毎に、3種類のリソースに分割しています。リソースを分割することで権限管理の対象が細分化されるため、RBAC(Role-based access control)を用いて「最小権限の原則」に基づいた安全な運用が可能です。
Kubernetes Gateway APIを構成する3種類のリソース
Gateway APIは、Kubernetesクラスタ外部からクラスタ内のService
へのL4/L7でのルーティングを、3種類リソース GatewayClass
、Gateway
、Route
を用いて定義するAPIです。
GatewayClass
Gateway
を構成するためのテンプレートを示すリソースGateway
を構成するために使用するGateway Controllerをパラメータと共に指定する- このパラメータで
Gateway
構成時に構築されるロードバランサーの設定項目(L4、L7、外部、内部等)を指定する
Gateway
- リクエストをクラスタ内へルーティングするルールを定義するリソース
- 指定した
GatewayClass
の定義を元にロードバランサーやプロキシ等を実際に構築する - クラスタ内のどこにルーティングするかは
Route
によって定義する
Route
以下、公式ドキュメントにある図に描かれているように、GatewayClass
、Gateway
、Route
(図ではHTTPRoute
)の3つのリソースを組み合わせることで、Service
へのルーティングを定義します。
2021年11月時点で、GKEやIstioを含む複数のプロジェクトがGateway APIで定義された挙動を実現するGateway Controllerを実装しています。
Gateway APIを用いる利点
Gateway APIにはIngress APIと比べて以下の利点があります。
- ルーティングの設定に必要最小限な権限をRBACで付与できる
- プロバイダー依存性の低いManifestでルーティングの設定を定義できる
- 拡張性が高い
順に説明します。
利点1:ルーティングの設定に必要最小限な権限をRBACで付与できる
前述の通り、Gateway APIではService
へのルーティングを、3種類の責務に対応したリソースGatewayClass
、Gateway
、Route
で定義します。リソースが分かれていることで、RBACを用いて「誰が何をできるのか」をリソース毎に管理できます。つまり、開発者の責務に対して必要なルーティング設定を行う権限のみを付与できます。こうすることで、Kubernetesのリソースを通して行うルーティング設定に「最小権限の原則」に基づいた運用を導入できます。Ingress APIでは、1つのリソースでロードバランサーとService
ヘのルーティングの定義を兼ねており、権限の分離はできませんでした。
例として、下図の公式ドキュメントの図が示すような、1つのロードバランサーに対して複数のNamespace
に存在する異なるアプリケーションを紐づけたシステムを考えます。このシステムで、クラスタ管理者とアプリケーション開発者の権限を分けてみましょう。
- Step1
- クラスタの管理者にはクラスタ内のアプリケーションが共通で利用するロードバランサーを管理できるように、
Gateway
リソースを閲覧、作成、編集、削除できる権限を与える - 一方、アプリケーション開発者の権限は
Gateway
リソースの閲覧のみに絞ることで、サービス全体で利用するロードバランサーを誤って削除できないようにする
- クラスタの管理者にはクラスタ内のアプリケーションが共通で利用するロードバランサーを管理できるように、
- Step2
- 各アプリケーション開発者へは、特定の
Namespace
でのみRoute
リソースの閲覧、作成、編集、削除できる権限を与える - その結果、開発者が管理している特定の
Namespace
配下のアプリケーションに対してのみ、ロードバランサーからトラフィックをどのように割り振るのかを管理できるようになる
- 各アプリケーション開発者へは、特定の
このように権限を分離することで、各アプリケーション開発者に必要最小限の権限を与え、安全に開発を進めることができます。
利点2:プロバイダー依存性の低いManifestでルーティングの設定を定義できる
Gateway APIでは、3種類の実装サポートレベル CORE
、EXTENDED
、OPTIONAL
が定義されています。そして、このサポートレベルは機能毎に設定されています。
CORE
- 全てのGateway Controllerで実装される重要な機能
- Gateway APIでManifestの仕様が定義されている
EXTENDED
- 全てのGateway Controllerで実装されるわけではないが、重要な機能
- Gateway APIでManifestの仕様が定義されている
CUSTOM
- プロバイダー依存のオプショナルな機能
- Gateway APIではCRを指定できるようにManifestの仕様が定義されており、プロバイダーは任意のCRを用いた機能を実装できる
上記の説明で用いている「重要な機能」とは、一般にL7ロードバランサーに備わっている機能(ヘッダーベースのルーティング等)で、プロバイダーに依らず可搬性のある機能を指します。Ingress
では独自のannotationを用いる手段しかサポートしていませんでした。なお、どういった機能がどのサポートレベルまで対応するのか、明確な判断基準は示されていません。機能のサポートレベルについて詳しく知りたい方はAPI仕様のドキュメントをご参照ください。
Gateway APIのリソースでは、Service
へのルーティングを管理するのに必要な機能が一通りCORE
、EXTENDED
で定義されています。例えば、Gateway
における静的IPやSSL証明書の指定、HTTPRoute
におけるヘッダーベース・パスベースのルーティングやトラフィック分割の指定等が含まれています。そのため、これらの必要となる機能は各プロバイダーで、ある程度等しく実装されることが期待できます。その結果、Gateway API利用時に使用するManifestを汎用的に使えるようになることも期待できます。必要な機能が汎用的なManifestの仕様として定義されていれば、Manifestを見れば実現されている機能が一目で分かる利点があります。
一方、従来のIngress APIでは非常にシンプルな機能を実現するための仕様しか定義されておらず、各プロバイダーがManifestに独自のannotationを定義して機能を拡張する必要がありました。その結果、プロバイダー毎に提供される機能、必要なManifestのフォーマットが大きく異なっていました。
これに対して、Gateway APIでは、より高度なルーティング管理機能をAPIで最初から定義することと、以下で説明するCRを用いた拡張方法を提供することで、このIngress APIの問題を解消しようとしています。
利点3:拡張性が高い
Gateway APIはCRを用いた拡張ができるように設計されています。拡張のために、Gateway APIで定義されているManifestの中にはCRを指定できるポイントがいくつか用意されています。Ingress APIでは、annotationで拡張するしか手段がありませんでした。annotationは単なる文字列であり、必要な項目が設定されているかの確認等のバリデーションはできません。また、Manifestにどんなannotationを設定できるのかを知るにはドキュメントを確認する必要がありました。一方、CRによる拡張はManifestのバリデーションが可能です。kubectl get crd
やkubectl explain
等でSpecを確認できるため、設定可能な項目を知るのも容易になっています。Gateway APIがサポートしているCRによる拡張は、Controller開発者、利用者双方にとって、より好ましい拡張方法と言えます。
Gateway APIのバージョンgateway.networking.k8s.io/v1alpha2
における、各種Route
ではspec.rules[].backendrefs[].kind
でService
の代わりににCRを指定できます。例えば、Cloud FunctionsへルーティングするためのCRを作成し、指定することを考えてみます。Gateway Controllerの実装者はCloud Functionsへのルーティングに必要な情報をCRD(Custom Resource Definition)として定義しておきます。CRDに指定した通りにCRが作成され、Route
リソースで指定された際には、CRの情報を元にCloud Functionsへルーティングできるよう、Gateway Controllerを実装します。それにより、利用者がCRを作成し、Route
リソースにCRを指定すれば、Cloud Functionsへのルーティングを実現できるようになります。このように、Gateway APIを拡張し、Cloud Functionsへのルーティングを設定する機能を実現できます。
この他にも、Gatewayではspec.listener.allowedRoutes[].kinds[]
でCRを指定でき、既存のRoute
以外の独自のRoute
リソースも使用できます。このように、Gateway APIではCRを用いた拡張がしやすいように配慮されています。
# HTTPRouteでspec.rules[].backendrefs[]を指定する例 apiVersion: gateway.networking.k8s.io/v1alpha2 kind: HTTPRoute metadata: name: example-route namespace: gateway-api-example-ns2 spec: parentRefs: - name: prod-gateway hostnames: - "example.com" rules: - backendRefs: - kind: Service # kindを指定できるのでCRを指定することも可能。 デフォルトではServiceを指定するようになっている。 name: example-svc # 対象とするkindのmetadata.nameを指定 port: 80 # kind: Serviceの場合は必須
GCPにおけるGateway APIの実装
ここまでのGateway APIに関する説明は、2021年11月時点でのGateway APIの最新バージョンgateway.networking.k8s.io/v1alpha2
に対するものです。本章で説明する、GKE Gateway Controllerはバージョンnetworking.x-k8s.io/v1alpha1
への対応となっており、動作検証ではバージョンnetworking.x-k8s.io/v1alpha1
で定義されたManifestを利用しているのでご注意ください。
GCPでは、そのnetworking.x-k8s.io/v1alpha1
に対応した、GKE Gateway Controllerがプレビュー機能として公開されています。GKE Gateway Controllerを利用することで、単一または複数のGKEクラスタにまたがる内部、外部HTTP(S)負荷分散を管理できます。2021年11月時点で、GatewayClass
、Gateway
、HTTPRoute
リソースのみがサポートされています。また、4種類のGatewayClassが定義されており、各GatewayClass
毎にサポートする機能が異なっています。
4種類のGatewayClass
は以下の通りです。
gke-l7-rilb
- シングルクラスタ内部ロードバランサー
gke-l7-rilb-mc
- マルチクラスタ内部ロードバランサー
gke-l7-gxlb
- シングルクラスタ外部ロードバランサー
gke-l7-gxlb-mc
- マルチクラスタ外部ロードバランサー
GKE Gateway Controllerの動作検証
ここまで、Gateway APIと、その実装であるGKE Gateway Controllerを紹介しました。本章では、GCP公式ドキュメントにある「Gateway のデプロイ」に従って、実際にGKE上でGateway APIを利用し、APIで定義されるトラフィックのルーティングがGKE Gateway Controllerによって、どのようにGCP上で実現されるのかを見ていきます。
1. GKE Gateway Controllerに対応したシングルクラスタを構築する
GKEで単一のKubernetesクラスタを構築し、Gateway APIを用いて、下図に示す内部負荷分散を実現します。コンテナネイティブ負荷分散を行うため、GKEクラスタはVPCネイティブクラスタである必要があります。
以下のサンプルのように、TerraformでGCP上に検証環境を構築します。なお、2021年11月時点で公式ドキュメント記載のGKE Gateway Controller利用可能リージョンにはasia-northeast1
は含まれていませんでしたが、試してみたところasia-northeast1
に構築したGKEクラスタでも利用できました。
# VPC作成 resource "google_compute_network" "gke_vpc" { name = "gke-vpc" auto_create_subnetworks = false } # 内部LBを作成する場合に必要なproxy only subnetを作成 # ref. https://cloud.google.com/load-balancing/docs/l7-internal/proxy-only-subnets resource "google_compute_subnetwork" "proxy_only_subnet" { name = "proxy-only-subnetwork" ip_cidr_range = "10.0.3.0/24" # cider for gke node region = "asia-northeast1" network = google_compute_network.gke_vpc.id purpose = "INTERNAL_HTTPS_LOAD_BALANCER" role = "ACTIVE" } # VPCネイティブクラスタを作成する際に指定するサブネットを雑に作成 # 本来は下記リンク先を参考にCIDRを要件に応じて設計するべきです # ref. https://cloud.google.com/kubernetes-engine/docs/how-to/flexible-pod-cidr resource "google_compute_subnetwork" "gke_subnet" { name = "gke-subnetwork" ip_cidr_range = "10.0.1.0/24" # cider for gke node region = "asia-northeast1" network = google_compute_network.gke_vpc.id secondary_ip_range { range_name = "gke-pod" ip_cidr_range = "10.0.0.0/24" } secondary_ip_range { range_name = "gke-service" ip_cidr_range = "10.0.2.0/24" } private_ip_google_access = true } # GKEのノードに割り当てるサービスアカウントを作成 resource "google_service_account" "gke_node_pool" { account_id = "gke-node-pool" display_name = "gke-node-pool" description = "A service account for GKE node" } # サービスアカウントに必要最低限のIAMロール(権限)を付与 resource "google_project_iam_member" "gke_node_pool" { for_each = toset([ "roles/logging.logWriter", "roles/monitoring.metricWriter", "roles/monitoring.viewer", "roles/datastore.owner", "roles/storage.objectViewer", ]) role = each.value member = "serviceAccount:${google_service_account.gke_node_pool.email}" } # GKEクラスタを定義、VCP-Native、公開クラスタ resource "google_container_cluster" "main" { name = "gke-cluster" location = "asia-northeast1-a" # デフォルトノードプールは削除して別途ノードプールを作成する remove_default_node_pool = true initial_node_count = 1 # クラスタを作成するVPC、subnetを指定 network = google_compute_network.gke_vpc.self_link subnetwork = google_compute_subnetwork.gke_subnet.self_link # vpc native clusterにするための設定 networking_mode = "VPC_NATIVE" ip_allocation_policy { cluster_secondary_range_name = "gke-pod" services_secondary_range_name = "gke-service" } } # GKEクラスタのノードを定義 resource "google_container_node_pool" "primary_nodes" { name = "node-pool" location = "asia-northeast1-a" cluster = google_container_cluster.main.name node_count = 1 node_config { machine_type = "e2-medium" metadata = { disable-legacy-endpoints = "true" } # アクセススコープでは全てのサービスへの権限を付与し,サービスアカウント側で付与する権限を絞る service_account = google_service_account.gke_node_pool.email oauth_scopes = [ "https://www.googleapis.com/auth/cloud-platform" ] } }
2. Gateway APIのCRDをインストールし、GatewayClass
が作成されることを確認する
手順 1. でGKEクラスタを作成したら、下記のようにkubectl
コマンドを用い、クラスタにGateway APIのCRD(Custom Resource Definition)をインストールします。CRDをインストールすると、GKE Gateway Controllerによって、GKEクラスタ内に自動的にシングルクラスタ用のGatewayClass
が作成されます。
$ CLUSTER_NAME=gke-cluster $ ZONE=asia-northeast1-a $ gcloud container clusters get-credentials $CLUSTER_NAME --zone $ZONE $ kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v0.3.0" \ | kubectl apply -f - customresourcedefinition.apiextensions.k8s.io/backendpolicies.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/gatewayclasses.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/gateways.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/httproutes.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/tcproutes.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/tlsroutes.networking.x-k8s.io created customresourcedefinition.apiextensions.k8s.io/udproutes.networking.x-k8s.io created $ kubectl get gatewayclass NAME CONTROLLER AGE gke-l7-gxlb networking.gke.io/gateway 11s gke-l7-rilb networking.gke.io/gateway 11s $ kubectl describe gatewayclass gke-l7-ril ~~~ Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ADD 20s sc-gateway-controller gke-l7-rilb
3. Gateway
リソースを作成することで、GCLBが作成されることを確認する
次に、Gateway
リソースを作成し、GKE Gateway Controller経由で内部ロードバランサーを作成します。
Gateway
では、spec.gatewayClassName
でGatewayClass
を指定します。そこでは、作成したいロードバランサーの種別に応じて適切なGatewayClass
を選択します。今回は、シングルクラスタ内部ロードバランサーを作成したいので、gke-l7-rilb
を指定します。そして、spec.listeners
で利用するプロトコル、ポート番号、Gateway
との紐付けを許可するRoute
の条件を指定します。
また、Gateway
で紐付けを許可するRoute
の条件を指定しますが、逆にRoute
側でも紐付けを許可するGateway
の条件を指定できます。そして、双方向に条件が満たされた場合にのみ、該当のGateway
とRoute
が紐づけられます。なお、Gateway
では、許可するRoute
の条件として、Route
のkind
、label
、hostnames
、Route
を作成するNamespace
のlabel
を指定できます。つまり、この条件を満たすRoute
を作成する権限があれば自由にルーティングルールを追加できることを意味します。Gateway APIでは、ルーティングルールを一元管理する仕組みが定義されていないので、ルーティングルールの適切な管理は運用でカバーする必要があります。
kind: Gateway apiVersion: networking.x-k8s.io/v1alpha1 metadata: name: internal-http spec: gatewayClassName: gke-l7-rilb listeners: - protocol: HTTP port: 80 routes: # 紐づけを許可するRouteの条件を指定 kind: HTTPRoute selector: matchLabels: gateway: internal-http
Gateway
リソースを作成すると、GKE Gateway Controllerにより、GCLB及びGCLBに紐づけられたバックエンドサービス、ヘルスチェックが新規に作成されます。
$ kubectl apply -f gateway.yaml gateway.networking.x-k8s.io/internal-http created $ kubectl get gateway NAME CLASS AGE internal-http gke-l7-rilb 39s $ kubectl describe gateway internal-http ~~~ Status: Addresses: Type: IPAddress Value: 10.0.1.4 Conditions: Last Transition Time: 1970-01-01T00:00:00Z Message: Waiting for controller Reason: NotReconciled Status: False Type: Scheduled Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ADD 4m1s sc-gateway-controller default/internal-http Warning SYNC 3m42s sc-gateway-controller generic::invalid_argument: error ensuring load balancer: Insert: The resource 'projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5' is not ready Normal UPDATE 17s (x3 over 4m1s) sc-gateway-controller default/internal-http Normal SYNC 17s sc-gateway-controller SYNC on default/internal-http was a success
# GCLBが作成されている $ gcloud compute url-maps list NAME DEFAULT_SERVICE gkegw-8r5w-default-internal-http-2jzr7e3xclhj # バックエンドサービスが作成されている $ gcloud compute backend-services list NAME BACKENDS PROTOCOL gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 HTTP # ヘルスチェックが作成されている $ gcloud compute health-checks list NAME PROTOCOL gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 HTTP
Gateway
リソースから作成されたロードバランサーはUI及びCLIから確認できます。しかしながら、公式ドキュメントには「Gatewayによって作成されたGoogle CloudロードバランサのリソースはGoogle Cloud Console UIに表示されません」と記載されています。正しい値が表示される保証はないのでご注意ください。
4. HTTPRoute
リソースを作成して、GCLBにルーティングのルールが追加されることを確認する
次に、Deployment
とService
を作成した上で、Service
とGateway
を紐づけるHTTPRoute
を作成します。
まず、以下のManifestで4組のDeployment
とService
を追加します。
apiVersion: apps/v1 kind: Deployment metadata: name: store-v1 spec: replicas: 2 selector: matchLabels: app: store version: v1 template: metadata: labels: app: store version: v1 spec: containers: - name: whereami image: gcr.io/google-samples/whereami:v1.1.3 ports: - containerPort: 8080 env: - name: METADATA value: "store-v1" --- apiVersion: v1 kind: Service metadata: name: store-v1 spec: selector: app: store version: v1 ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: store-v2 spec: replicas: 2 selector: matchLabels: app: store version: v2 template: metadata: labels: app: store version: v2 spec: containers: - name: whereami image: gcr.io/google-samples/whereami:v1.1.3 ports: - containerPort: 8080 env: - name: METADATA value: "store-v2" --- apiVersion: v1 kind: Service metadata: name: store-v2 annotations: # BackendConfigを用いてヘルスチェック等をサービス毎にカスタマイズする cloud.google.com/backend-config: '{"default": "store-v2-backendconfig"}' spec: selector: app: store version: v2 ports: - port: 8080 targetPort: 8080 --- apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: store-v2-backendconfig spec: healthCheck: checkIntervalSec: 15 port: 8080 type: HTTP requestPath: /v2 connectionDraining: drainingTimeoutSec: 60 --- apiVersion: apps/v1 kind: Deployment metadata: name: store-german spec: replicas: 2 selector: matchLabels: app: store version: german template: metadata: labels: app: store version: german spec: containers: - name: whereami image: gcr.io/google-samples/whereami:v1.1.3 ports: - containerPort: 8080 env: - name: METADATA value: "Gutentag!" --- apiVersion: v1 kind: Service metadata: name: store-german annotations: # BackendConfigを用いてヘルスチェック等をサービス毎にカスタマイズする cloud.google.com/backend-config: '{"default": "store-german-backendconfig"}' spec: selector: app: store version: german ports: - port: 8080 targetPort: 8080 --- apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: store-german-backendconfig spec: healthCheck: checkIntervalSec: 15 port: 8080 type: HTTP requestPath: /healthz connectionDraining: drainingTimeoutSec: 60 --- apiVersion: apps/v1 kind: Deployment metadata: name: store-mirror-target spec: replicas: 2 selector: matchLabels: app: store version: mirror-target template: metadata: labels: app: store version: mirror-target spec: containers: - name: whereami image: gcr.io/google-samples/whereami:v1.1.3 ports: - containerPort: 8080 env: - name: METADATA value: "store-mirror-target" --- apiVersion: v1 kind: Service metadata: name: store-mirror-target spec: selector: app: store version: store-mirror-target ports: - port: 8080 targetPort: 8080
このマニフェストをstore-deployment-service.yaml
というファイル名で保存し、applyします。
$ kubectl apply -f store-deployment-service.yaml deployment.apps/store-v1 created service/store-v1 created deployment.apps/store-v2 created service/store-v2 created deployment.apps/store-german created service/store-german created deployment.apps/store-mirror-target created service/store-mirror-target created $ kubectl get pod --show-labels NAME READY STATUS RESTARTS AGE LABELS store-german-66dcb75977-4lnkf 1/1 Running 0 86m app=store,pod-template-hash=66dcb75977,version=german store-german-66dcb75977-plqtx 1/1 Running 0 86m app=store,pod-template-hash=66dcb75977,version=german store-mirror-target-c6b945fdf-4tqj9 1/1 Running 0 86m app=store,pod-template-hash=c6b945fdf,version=mirror-target store-mirror-target-c6b945fdf-9lnbt 1/1 Running 0 86m app=store,pod-template-hash=c6b945fdf,version=mirror-target store-v1-65b47557df-5m6xc 1/1 Running 0 86m app=store,pod-template-hash=65b47557df,version=v1 store-v1-65b47557df-65p42 1/1 Running 0 86m app=store,pod-template-hash=65b47557df,version=v1 store-v2-6856f59f7f-cczqb 1/1 Running 0 86m app=store,pod-template-hash=6856f59f7f,version=v2 store-v2-6856f59f7f-dsbnc 1/1 Running 0 86m app=store,pod-template-hash=6856f59f7f,version=v2 $ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.0.2.1 <none> 443/TCP 101m store-german ClusterIP 10.0.2.204 <none> 8080/TCP 86m store-mirror-target ClusterIP 10.0.2.165 <none> 8080/TCP 86m store-v1 ClusterIP 10.0.2.35 <none> 8080/TCP 86m store-v2 ClusterIP 10.0.2.89 <none> 8080/TCP 86m
このstore-deployment-service.yaml
に記載の通り、GKE Gateway Controllerでは、GLBC同様にBackendConfig
リソースを用いて、Service
毎にヘルスチェックやコネクションドレイニングの設定を変更できます。しかし、この機能はGA前に別のリソースに置き換えられることがドキュメントに明記されています。そして、10月にリリースされ、GKE Gateway ControllerではまだサポートされていないGateway API Version gateway.networking.k8s.io/v1alpha2
においては、ヘルスチェック等を定義するための仕組みとしてPolicy Attachmentが定義されています。BackendConfig
リソースはこの仕組みを利用するリソースに置き換えられると考えられます。
また、HTTPRoute
では、spec.gateways
で処理を担当するホスト名、spec.gateways
で紐付けを許可するGateway
の条件、spec.rules
でリクエストをどのように処理するかのルールを指定できます。
spec.rules[].matches
- リクエストのパス、ヘッダー、クエリパラメータでルールを適用する対象のリクエストを指定
spec.rules[].forwardTo
spec.rules[].matches
で指定した条件に合致するリクエストをルーティングする先を指定- ルーティング先として、複数のサービス、portの組みを指定可能
- 複数サービス間でのルーティングの分割割合も指定可能
spec.rules[].filter
spec.rules[].matches
で指定した条件に合致するリクエストのヘッダー修正、ミラーリングを指定
以下、Manifestで定義されるHTTPRoute store
によって、再掲する下図に示すルーティングルールを設定します。
kind: HTTPRoute apiVersion: networking.x-k8s.io/v1alpha1 metadata: name: store labels: gateway: internal-http spec: hostnames: - "store.example.com" rules: - matches: - path: type: Prefix value: /de forwardTo: - serviceName: store-german port: 8080 filters: # /deへのリクエストをService store-mirror-targetにミラーリングする - type: RequestMirror requestMirror: serviceName: store-mirror-target port: 8080 - matches: - path: type: Prefix value: /mirror forwardTo: - serviceName: store-mirror-target port: 8080 - matches: - headers: type: Exact values: env: canary forwardTo: - serviceName: store-v2 port: 8080 # matches未指定のルールは、合致するmatchesが存在しないリクエストに対して適用される - forwardTo: - serviceName: store-v1 port: 8080 # このルールが適用されるリクエストの9割をService store-v1にルーティングする weight: 90 - serviceName: store-v2 port: 8080 # このルールが適用されるリクエストの1割をService store-v2にルーティングする weight: 10
HTTPRoute
リソースを作成し、しばらく待ちます。すると、GKE Gateway Controllerによって、ルーティング先に指定した4つのサービス毎にバックエンドサービス、ヘルスチェックならびにNEG(Network Endpoint Group)が新規に作成されます。
NEGはKubernetesクラスタ内に動的に作成されるService
、Pod
と直接通信できるエンドポイントを管理し、VPC内に提供する仕組みです。NEGの詳細は、以下記事が分かりやすいのでご参照ください。
medium.com
$ kubectl apply -f store-route.yaml httproute.networking.x-k8s.io/store created $ kubectl get httproute NAME HOSTNAMES AGE store ["store.example.com"] 25s $ kubectl describe httproute store Name: store Namespace: default Labels: gateway=internal-http Annotations: <none> API Version: networking.x-k8s.io/v1alpha1 Kind: HTTPRoute ~~~ Status: Gateways: Conditions: Last Transition Time: 2021-11-10T06:47:18Z Message: Reason: RouteAdmitted Status: True Type: Admitted Last Transition Time: 2021-11-10T06:47:18Z Message: Reason: ReconciliationSucceeded Status: True Type: Reconciled Gateway Ref: Name: internal-http Namespace: default Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ADD 90s sc-gateway-controller default/store Normal SYNC 7s sc-gateway-controller Bind of HTTPRoute "default/store" to Gateway "default/internal-http" was a success Normal SYNC 7s sc-gateway-controller Reconciliation of HTTPRoute "default/store" bound to Gateway "default/internal-http" was a success
そして、GCLBにはHTTPRoute
で指定した各サービスへのルーティングのルールが追加されます。GCLBに機能はあるものの、GKE Ingress Controllerでは実現できなかった、ルーティングの分割割合の指定等が設定できていることが確認できます。
$ gcloud compute url-maps list NAME DEFAULT_SERVICE gkegw-8r5w-default-internal-http-2jzr7e3xclhj # バックエンドサービスが4つ追加されており、それぞれのBACKENDSにNEGが指定されている $ gcloud compute backend-services list NAME BACKENDS PROTOCOL gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 HTTP gkegw-8r5w-default-store-german-8080-o9g73h4mk3ob asia-northeast1-a/networkEndpointGroups/k8s1-8db9299d-default-store-german-8080-e803f15f HTTP gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r asia-northeast1-a/networkEndpointGroups/k8s1-8db9299d-default-store-mirror-target-8080-de687243 HTTP gkegw-8r5w-default-store-v1-8080-t7d6vxl1jy1d asia-northeast1-a/networkEndpointGroups/k8s1-8db9299d-default-store-v1-8080-52e6fd60 HTTP gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c asia-northeast1-a/networkEndpointGroups/k8s1-8db9299d-default-store-v2-8080-70e3804f HTTP # ヘルスチェックも新たに4つ追加されている $ gcloud compute health-checks list NAME PROTOCOL gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 HTTP gkegw-8r5w-default-store-german-8080-o9g73h4mk3ob HTTP gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r HTTP gkegw-8r5w-default-store-v1-8080-t7d6vxl1jy1d HTTP gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c HTTP # store-deployment-service.yamlで追加した、Serviceに対応するNEGが新たに4つ追加されている $ gcloud compute network-endpoint-groups list NAME LOCATION ENDPOINT_TYPE SIZE k8s1-8db9299d-default-store-german-8080-e803f15f asia-northeast1-a GCE_VM_IP_PORT 2 k8s1-8db9299d-default-store-mirror-target-8080-de687243 asia-northeast1-a GCE_VM_IP_PORT 2 k8s1-8db9299d-default-store-v1-8080-52e6fd60 asia-northeast1-a GCE_VM_IP_PORT 2 k8s1-8db9299d-default-store-v2-8080-70e3804f asia-northeast1-a GCE_VM_IP_PORT 2
そして、HTTPRoute
で指定したルーティングのルールが、GCLBに追加されていることが確認できます。
$ gcloud compute url-maps describe gkegw-8r5w-default-internal-http-2jzr7e3xclhj --region asia-northeast1 creationTimestamp: '2021-11-12T01:46:24.204-08:00' defaultRouteAction: faultInjectionPolicy: abort: httpStatus: 404 percentage: 100.0 weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 weight: 1 fingerprint: AczSXReW744= hostRules: - hosts: - store.example.com pathMatcher: hostffxyqcv3l2rgbj3v3jakx7trkfuw01ei id: '1800894912999426335' kind: compute#urlMap name: gkegw-8r5w-default-internal-http-2jzr7e3xclhj pathMatchers: - defaultRouteAction: faultInjectionPolicy: abort: httpStatus: 404 percentage: 100.0 weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 weight: 1 name: hostffxyqcv3l2rgbj3v3jakx7trkfuw01ei routeRules: - matchRules: - prefixMatch: /mirror priority: 1 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r weight: 1 - matchRules: - prefixMatch: /de priority: 2 routeAction: requestMirrorPolicy: backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-german-8080-o9g73h4mk3ob weight: 1 - matchRules: - prefixMatch: /de priority: 3 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 1 - matchRules: - headerMatches: - exactMatch: canary headerName: env prefixMatch: / priority: 4 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 1 - matchRules: - prefixMatch: / priority: 5 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v1-8080-t7d6vxl1jy1d weight: 90 - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 10 region: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1 selfLink: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/urlMaps/gkegw-8r5w-default-internal-http-2jzr7e3xclhj
実際に、HTTPRoute
で指定した条件に合致するリクエストを飛ばすと、パスベース、ヘッダーベースのルーティング、トラフィック分割ルールに従ってルーティングされることが確認できます。
# Gateway(内部GCLB)に付与されたIPを確認 $ kubectl get gateway internal-http -o=jsonpath="{.status.addresses[0].value}" 10.0.1.4 $ kubectl run curlpod --image curlimages/curl:7.78.0 --command -- sleep 3600 $ kubectl exec curlpod -it -- /bin/sh # トラフィック分割によって、store-v1、時折store-v2にルーティングされることを確認 / $ curl -H "host: store.example.com" 10.0.1.4 "pod_name": "store-v1-65b47557df-5m6xc" / $ curl -H "host: store.example.com" 10.0.1.4 "pod_name": "store-v1-65b47557df-65p42" ~~~ / $ curl -H "host: store.example.com" 10.0.1.4 "pod_name": "store-v2-6856f59f7f-cczqb" # ヘッダーベースのルーティングでstore-v2にルーティングされることを確認 / $ curl -H "host: store.example.com" -H "env: canary " 10.0.1.4 "pod_name": "store-v2-6856f59f7f-cczqb" # パスベースのルーティングでstore-germanにルーティングされることを確認 / $ curl -H "host: store.example.com" 10.0.1.4/de "pod_name": "store-german-66dcb75977-plqtx" # パス/deへのリクエストがService store-mirror-targetにミラーリングされていることを、Podで出力しているアクセスログから確認 $ kubectl logs store-mirror-target-c6b945fdf-9lnbt ~~~ 2021-11-12 11:00:25,291 - werkzeug - INFO - 10.0.3.37 - - [12/Nov/2021 11:00:25] "GET /de HTTP/1.1" 200 - ~~~
5. HTTPRoute
で定義したルーティングルールの優先順位とGCLB上でのルールの優先順位の定義を確認する
また、GKE Gateway Controllerではspec.rules[].matches
をドキュメント記載の以下の基準に従って優先順位付けします。
- ホスト
- 最も長い、または最も具体的なホスト名と一致するものを優先
- パス
- 最も長い、または最も具体的なパスと一致するものを優先
- ヘッダー
- 一致するHTTPヘッダーの数が多いものを優先
リクエストに合致するルーティングルールが複数ある場合、より優先度の高いものが適用されます。また、spec.rules[].matches
が全く同じルーティングルールが存在する場合は、作成されたタイムスタンプがより古いルーティングルールが適用されます。
以下が検証のサンプルです。既存のHTTPRoute
に定められたパスベースによるルーティングに競合するルールを追加し、ルーティングルールの優先順位を検証します。
kind: HTTPRoute apiVersion: networking.x-k8s.io/v1alpha1 metadata: name: store-conflict labels: gateway: internal-http spec: hostnames: - "store.example.com" rules: # /deでのパスベースのルーティングはHTTPRoute storeで既に定義されているため、競合する - matches: - path: type: Prefix value: /de forwardTo: - serviceName: store-v2 port: 8080
# 競合するルーティングルールを持つHTTPRouteでも正常にapplyできる $ kubectl apply -f store-route-conflict.yaml httproute.networking.x-k8s.io/store-conflict created $ kubectl get httproute NAME HOSTNAMES AGE store ["store.example.com"] 66m store-conflict ["store.example.com"] 13s
ヘッダー、パスを両方指定した場合、パスベースの条件の方がヘッダーベースの条件よりも優先されることが確認できます。また、競合するルーティングルールが存在するパス/de
を指定した場合は、競合するルール群の中で最初に作成されたものが適用されることを確認できます。
# ヘッダー、パスを両方指定した場合、パスベースの条件の方がヘッダーベースの条件よりも優先され、 # store-germanにルーティングされることを確認 $ curl -H "host: store.example.com" -H "env: canary " 10.0.1.4/de "pod_name": "store-german-66dcb75977-plqtx" # 競合するルーティングルールがあるパスを指定した場合、競合するルールの内、先に作成したstore-germanへのルーティングルールが適用され、 # 後から作成したstore-v2へのルーティングルールは適用されないことを確認 $ curl -H "host: store.example.com" 10.0.1.4/de "pod_name": "store-german-66dcb75977-4lnkf"
次に、GCLBに設定されたルーティングルールの優先順位を確認します。ルールの優先順位はGCLBに定義されたrouteRule
のpriority
に設定されていることが分かります。
$ gcloud compute url-maps describe gkegw-8r5w-default-internal-http-2jzr7e3xclhj --region asia-northeast1 creationTimestamp: '2021-11-12T01:46:24.204-08:00' defaultRouteAction: faultInjectionPolicy: abort: httpStatus: 404 percentage: 100.0 weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 weight: 1 fingerprint: AczSXReW744= hostRules: - hosts: - store.example.com pathMatcher: hostffxyqcv3l2rgbj3v3jakx7trkfuw01ei id: '1800894912999426335' kind: compute#urlMap name: gkegw-8r5w-default-internal-http-2jzr7e3xclhj pathMatchers: - defaultRouteAction: faultInjectionPolicy: abort: httpStatus: 404 percentage: 100.0 weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-gw-serve404-80-mcfti8ucx6x5 weight: 1 name: hostffxyqcv3l2rgbj3v3jakx7trkfuw01ei routeRules: - matchRules: - prefixMatch: /mirror priority: 1 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r weight: 1 - matchRules: - prefixMatch: /de priority: 2 routeAction: requestMirrorPolicy: backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-mirror-target-8080-zcxtgvjcck1r weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-german-8080-o9g73h4mk3ob weight: 1 - matchRules: - prefixMatch: /de priority: 3 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 1 - matchRules: - headerMatches: - exactMatch: canary headerName: env prefixMatch: / priority: 4 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 1 - matchRules: - prefixMatch: / priority: 5 routeAction: weightedBackendServices: - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v1-8080-t7d6vxl1jy1d weight: 90 - backendService: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/backendServices/gkegw-8r5w-default-store-v2-8080-sau4ah4scq2c weight: 10 region: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1 selfLink: https://www.googleapis.com/compute/v1/projects/techblog/regions/asia-northeast1/urlMaps/gkegw-8r5w-default-internal-http-2jzr7e3xclhj
競合するルーティングルールがあった場合でも、Gateway
,HTTPRoute
リソースのStatusがエラー等になることはありません。Gateway
とRoute
は多対多の紐付けが可能なため、複数箇所でHTTPRoute
を定義した結果、気づかないうちに競合するルーティングルールを定義しないように注意が必要です。しかし、Gateway APIではルーティングルールを一元管理する仕組みは特に定義されていません。そのため、ルーティングルールの競合への対応方針としてGateway APIのドキュメントに以下の記載があります。
Where possible, this should be communicated by setting appropriate status conditions on relevant resources.
GKE Gateway ControllerがGAになる際には、Gateway APIで定義されるリソースのStatusに警告が表示されるようになるかもしれません。
おわりに
本記事では、Kubernetes Gateway APIの概要と、APIで定義されるトラフィックのルーティングがGKE Gateway ControllerによってどのようにGCP上で実現されるのかの仕組みを紹介しました。Kubernetes Gateway APIとRBACを組み合わせることで、よりセキュアなマルチテナント構成を実現できます。そして、GKE Ingress ControllerではなかなかサポートされなかったGCLBの各種機能がGKE Gateway Controllerでサポートされるようなので、GAになるのが非常に楽しみです。
ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!