🔀

GKE Gateway controller の新機能を試してみた カスタムヘッダー, URL リライト, IAP 編

2023/08/28に公開

こんにちは。クラウドエースの阿部です。
こちらのブログ記事では、前回のブログ記事に引き続き、2023年7月10日に追加された Gateway controller の新機能について紹介します。

概要

2023年7月10日に、 GKE の Gateway controller に以下の機能が追加されました。

  • リージョンアプリケーションロードバランサの GatewayClass が追加
  • Identity-aware Proxy (IAP) 連携機能が追加
  • カスタムリクエストヘッダ、カスタムレスポンスヘッダ機能が追加
  • URL リライトとパスリダイレクト機能が追加

今回のブログ記事ではこのうち、「Identity-aware Proxy (IAP) 連携機能」「カスタムリクエストヘッダ、カスタムレスポンスヘッダ機能」「URL リライトとパスリダイレクト機能」の3つについて具体的なサンプルを交えて説明していきます。

GKE Gateway controller (Gateway API) って何?という方は、下記のブログ記事もご一読ください。

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

環境準備

OAuth 同意画面の構成

今回のブログ記事では、 IAP を使った検証を行います。IAP を使用するためには、最初に OAuth 同意画面を構成する必要があります。
下記のドキュメントに記載されている「OAuth 同意画面の構成」の手順に従い、設定を行ってください。(これを設定しないと、 Terraform で OAuth Client を作成する際に失敗する場合があります)

https://cloud.google.com/iap/docs/enabling-kubernetes-howto?hl=ja#oauth-configure

なお、OAuth 認証情報や IAP は実際の検証を行うときに設定します。

Google Cloud リソース作成

前回のブログ記事と同様に、Terraform を使って検証環境を構築できるサンプルコードを用意しました。前提コマンドライン等は前回のブログ記事の内容をご確認ください。(前回のブログ記事で用意したリポジトリと同一です。)

下記の要領で適当なディレクトリにサンプルコードをダウンロードしてください。

git clone https://github.com/cloud-ace/zenn-gke-gateway-policy-sample
cd zenn-gke-gateway-policy-sample/terraform

※既にリポジトリをクローン済の方は、 git pull で更新して頂いてもOKです

次に、下記のコマンドを実行し、サンプル環境を作成します。
export TF_VAR_project_id=XXXXXXXXXXXX 箇所は、使用可能なプロジェクトIDを入力してください。
前回のブログ記事と異なる点は、 export TF_VAR_enable_iap=true という環境変数を設定している点です。今回は、IAP の検証を行うため、IAP 用の OAuth クライアント設定も Terraform で作成するようにコードを修正しました。
terraform apply 実行後に Enter a value: が表示されたら、 yes と入力しEnterキーを押してください。

export TF_VAR_project_id=XXXXXX
export TF_VAR_enable_iap=true
terraform init
terraform apply

terraform apply が完了すると、以下のように Outputs: でFQDNが表示されるはずです。このFQDNは検証で使用するため、テキストエディタ等にコピーしておきましょう。

Outputs:

endpoint_fqdn = "gke-gateway-********.endpoints.PROJECT_ID.cloud.goog"

以下のような図のリソースが一通り作成されます。

resource diagram

主なリソースは前回のブログ記事で構築したリソースと同じですが、よく見ると google_iap_client というリソースが追加になっていることが分かると思います。

共通で使用する Kubernetes オブジェクト

GKE クラスタ作成後、 kubectl コマンドを使って Kubernetes オブジェクトをデプロイします。
今回は、 kubernetes2 というディレクトリにある YAML ファイルを使用します。 ※/kubernetes は前回ブログ記事のサンプルコードディレクトリです。

cd ../kubernetes2

上記ディレクトリに移動後、 base にある設定を投入します。

kubectl apply -f base

作成される Kubernetes オブジェクトは前回ブログ記事と同等です。異なる点として、 store-v1store-v2store-germanのサンプルアプリにECHO_HEADERS環境変数を付与することで、各アプリの応答にリクエストヘッダ情報を追加表示する動作に変更しています。

Kubernetes オブジェクト オブジェクト名
Namespace gateway-infra
Deployment store-v1
Deployment store-v2
Deployment store-german
Service store-v1
Service store-v2
Service store-german

実際に Kubernetes オブジェクトが作成されたことを確認する手順は前回ブログ記事の内容を確認してください。(今回のブログ記事では記載を割愛します。)

カスタムリクエストヘッダ、カスタムレスポンスヘッダ

カスタムリクエストヘッダは、HTTPクライアントのリクエストに Gateway (ロードバランサ)で独自のヘッダを付与したり、クライアントが付与したリクエストヘッダの内容を変更する機能です。
また、カスタムレスポンスヘッダはアプリケーションの応答するレスポンスに独自のヘッダを付与したり、アプリケーションのヘッダを変更する機能です。

検証

以下のコマンドを実行することで、サンプル設定を投入することができます。

kubectl apply -f custom_headers

※設定を入れてから通信が有効になるまで10分程度時間がかかることがあります。

この設定を入れることで、以下のようなカスタムヘッダを追加設定しています。

パス アプリ カスタムリクエストヘッダ カスタムレスポンスヘッダ
/v1-custom store-v1 X-Version
X-Client-IP-Address
X-Client-Geo-Location
/v2-custom store-v2 X-version
X-Client-RTT
server

store-v1 のエンドポイントへのリクエスト

まずは、 //v1-custom で比較してみましょう。

## / へのリクエスト
curl https://gke-gateway-********.endpoints.PROJECT_ID.cloud.goog

## /v1-custom へのリクエスト
curl https://gke-gateway-********.endpoints.PROJECT_ID.cloud.goog/v1-custom

/ のレスポンス

{
  "cluster_name": "gke-gtw-test",
  "gce_instance_id": "86384680349836962",
  "gce_service_account": "********.svc.id.goog",
  "headers": {
    "Accept": "*/*",
    "Host": "gke-gateway-********.endpoints.********.cloud.goog",
    "User-Agent": "curl/7.68.0",
    "Via": "1.1 google",
    "X-Cloud-Trace-Context": "8091931c24b8cd12228ab004acc2750f/1682628611239114639",
    "X-Forwarded-For": "***.***.***.***,***.***.***.***",
    "X-Forwarded-Proto": "https"
  },
  "host_header": "gke-gateway-********.endpoints.********.cloud.goog",
  "metadata": "store-v1",
  "pod_name": "store-v1-7fffb869fc-pld6b",
  "pod_name_emoji": "👩🏿‍❤️‍💋‍👩🏼",
  "project_id": "********",
  "timestamp": "2023-08-22T02:29:33",
  "zone": "asia-northeast1-a"
}

※プロジェクトID等はマスクしています。また、応答をjqコマンドで整形しています。

/v1-custom のレスポンス

{
  "cluster_name": "gke-gtw-test",
  "gce_instance_id": "86384680349836962",
  "gce_service_account": "********.svc.id.goog",
  "headers": {
    "Accept": "*/*",
    "Host": "gke-gateway-********.endpoints.********.cloud.goog",
    "User-Agent": "curl/7.68.0",
    "Via": "1.1 google",
    "X-Client-Geo-Location": "JP,Chiyoda City",
    "X-Client-Ip-Address": "***.***.***.***",
    "X-Cloud-Trace-Context": "6ca1a4869ec983a8f4bfa66da9e418f0/6119733550302643648",
    "X-Forwarded-For": "***.***.***.***,***.***.***.***",
    "X-Forwarded-Proto": "https",
    "X-Version": "v1"
  },
  "host_header": "gke-gateway-********.endpoints.********.cloud.goog",
  "metadata": "store-v1",
  "pod_name": "store-v1-7fffb869fc-pld6b",
  "pod_name_emoji": "👩🏿‍❤️‍💋‍👩🏼",
  "project_id": "********",
  "timestamp": "2023-08-22T02:31:32",
  "zone": "asia-northeast1-a"
}

※プロジェクトID等はマスクしています。また、応答をjqコマンドで整形しています。

レスポンスの比較

各アプリで、 headers の部分がリクエストヘッダに相当します。
比較すると、/v1-custom へのリクエストで、X-Client-Geo-LocationX-Client-Ip-AddressX-Version が追加されていることがわかります。
これらが、追加のレスポンスです。

store-v2 のエンドポイントへのリクエスト

次に、 /v2 と /v2-custom のレスポンスを比較します。今回はレスポンスヘッダのみ確認したいため、curl コマンドに -I オプションを付与しています。

## /v2 へのリクエスト
curl -I https://gke-gateway-********.endpoints.PROJECT_ID.cloud.goog/v2

## /v2-custom へのリクエスト
curl -I https://gke-gateway-********.endpoints.PROJECT_ID.cloud.goog/v2-custom

/v2 のレスポンス

HTTP/2 200 
server: Werkzeug/2.3.7 Python/3.11.3
date: Tue, 22 Aug 2023 02:40:07 GMT
content-type: application/json
content-length: 662
access-control-allow-origin: *
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

/v2-custom のレスポンス

HTTP/2 200 
server: anonymous
date: Tue, 22 Aug 2023 02:40:28 GMT
content-type: application/json
content-length: 662
access-control-allow-origin: *
via: 1.1 google
x-client-rtt: 5
x-version: v2
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

レスポンスの比較

比較すると、 /v2-custom レスポンスの方に、x-client-rttx-versionヘッダが付与されていることがわかります。また、 server ヘッダが Werkzeug フレームワークを示すものから、anonymous という匿名を示す文字列に変更されています。

設定のポイント

カスタムリクエストヘッダの設定のポイントは以下の箇所です。

https://github.com/cloud-ace/zenn-gke-gateway-policy-sample/blob/main/kubernetes2/custom_headers/http_route.yaml#L40-L49

matches と同じインデント位置に filters 属性を付与して、 type: RequestHeaderModifier を設定しています。この設定で、カスタムリクエストヘッダを実現しています。
また、 X-Client-IP-AddressX-Client-Geo-Location は固定値ではなく {client_ip_address} のようなブレースで変数を設定しています。この変数はKubernetes Gateway API ではなくアプリケーションロードバランサの機能で、こちらのドキュメントに設定可能な変数が掲載されています。

同様に、カスタムレスポンスヘッダの設定のポイントは以下の箇所です。

https://github.com/cloud-ace/zenn-gke-gateway-policy-sample/blob/main/kubernetes2/custom_headers/http_route.yaml#L62-L69

filters 属性に type: ResponseHeaderModifier を設定しています。設定の要領はカスタムリクエストヘッダと同等です。また、カスタムリクエストヘッダと同様に、ブレースにより変数を設定できます。
レスポンスヘッダの設定例には、 add だけではなく set も使用しています。 set はリクエストやレスポンスに同名のヘッダが有る場合に上書きしたい場合に使用する設定です。

クリーンアップ

次の検証に移る前に、以下のコマンドで設定を掃除してください。

kubectl delete -f custom_headers

URL リライト、パスリダイレクト

URL リライト(URL Rewrite)は、クライアントからのリクエストのURLをアプリケーションに渡す際に、ロードバランサで変更する機能です。
また、パスリダイレクトは、クライアントからのリクエストで条件に一致したとき、パスの変更をリダイレクトとしてクライアントに通知する機能です。
2つの機能は似ていますが、URL リライトはクライアントに通知せずロードバランサで処理が完結し、パスリダイレクトはクライアントに通知して別のパスにリクエストさせる点が異なります。また、前回ブログ記事で紹介した HTTP to HTTPS リダイレクトはプロトコル自体を変更する際に使用しますが、パスリダイレクトはプロトコルは変更せず、パスのみを変更したいときに使用します。

検証

以下のコマンドを実行することで、サンプル設定を投入することができます。

kubectl apply -f rewrite

上記設定では、以下のような HTTPRoute 設定を行っています。

  • /v1 を含むパスにアクセスしたら、 /v2 のパスにリダイレクトする
  • /v1-test を含むパスにアクセスしたら、ホスト名を test-domain に変更し、 /v1 パスに修正する

/v1 パスへのアクセス

下記のリクエストを出します。リダイレクト動作の確認のため、 -i オプションでレスポンスヘッダを表示します。

curl -i https://gke-gateway-********.endpoints.********.cloud.goog/v1

下記のようなレスポンスヘッダが返ってきます。location ヘッダに、 /v2 パスのリダイレクト先が出力されていることが確認できます。

HTTP/2 302 
cache-control: private
location: https://gke-gateway-********.endpoints.********.cloud.goog/v2
content-length: 0
date: Tue, 22 Aug 2023 08:41:31 GMT
content-type: text/html; charset=UTF-8
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

/v1-test パスへのアクセス

下記のリクエストを出します。パスを /v1-test/api としています。

curl https://gke-gateway-********.endpoints.********.cloud.goog/v1-test/api

下記の様なレスポンスが返ってきます。Host ドメインの部分が、 curl に指定したFQDNではなく test-domain に変わっていることが分かります。

{
  "cluster_name": "gke-gtw-test",
  "gce_instance_id": "86384680349836962",
  "gce_service_account": "********.svc.id.goog",
  "headers": {
    "Accept": "*/*",
    "Host": "test-domain",
    "User-Agent": "curl/7.68.0",
    "Via": "1.1 google",
    "X-Cloud-Trace-Context": "0dc731194c054b5c76d593ab7a9d12ca/4853906745609931812",
    "X-Forwarded-For": "***.***.***.***,***.***.***.***",
    "X-Forwarded-Proto": "https"
  },
  "host_header": "test-domain",
  "metadata": "store-v1",
  "pod_name": "store-v1-7fffb869fc-pld6b",
  "pod_name_emoji": "👩🏿‍❤️‍💋‍👩🏼",
  "project_id": "********",
  "timestamp": "2023-08-22T08:51:27",
  "zone": "asia-northeast1-a"
}

また、ログを確認してみます。 /api というパスを含んだログに絞っています。

kubectl logs store-v1-7fffb869fc-pld6b | grep /api | tail -1

kubectl logs に指定する Pod 名は、今回得られたレスポンスボディを使って指定しています。実用的には、 stern 等のコマンドを使ってPodのログを広く拾った方がよいでしょう。

以下のようなログを拾えました。パスが /v1-test/api から /v1/api に変更できています。

[2023-08-22 08:51:27,336] INFO in _internal: ***.***.***.*** - - [22/Aug/2023 08:51:27] "GET /v1/api HTTP/1.1" 200 -

設定のポイント

パスリダイレクトの設定は以下の箇所です。filterstype: RequestRedirect を設定し、何のステータスコードでどのパスにリダイレクトするかを設定します。

https://github.com/cloud-ace/zenn-gke-gateway-policy-sample/blob/main/kubernetes2/rewrite/http_route.yaml#L34-L40

また、URL リライトの設定は以下の箇所です。filterstype: URLRewrite を設定し、hostname と 書き換えるパスを指定すればよいです。

https://github.com/cloud-ace/zenn-gke-gateway-policy-sample/blob/main/kubernetes2/rewrite/http_route.yaml#L44-L50

クリーンアップ

次の検証に移る前に、以下のコマンドで設定を掃除してください。

kubectl delete -f rewrite

Identity-aware Proxy (IAP)

IAP は、ロードバランサのバックエンドサービスに認可プロキシを追加することで、簡単に Google アカウントによるアクセス制御を追加することが可能なサービスです。詳細は下記のドキュメントを参照してください。

https://cloud.google.com/iap/docs/concepts-overview

検証の準備

これまでと異なり、パラメータを編集して作成する作業が多いため、間違えないようにご注意ください。

OAuth Client ID の取得と置換

検証環境の構築で、 OAuth 同意画面と OAuth 認証情報(OAuth Client)は設定できているはずです。
最初に、以下のコマンドで OAuth 同意画面のリソース名を取得します。

gcloud iap oauth-brands list

このコマンドにより、以下のような結果が得られるはずです。

---
applicationTitle: OAuth 同意画面タイトル
name: projects/PROJECT_NUMBER/brands/PROJECT_NUMBER
orgInternalOnly: true
supportEmail: EMAIL_ADDRESS

この中で、name: 行の projects で始まる文字列の部分をテキストエディタ等にコピーします。(以降、OAUTH_BRANDという名前とします。)
次に、以下のコマンドで Terraform で作成した OAuth Client の情報を取得します。

OAUTH_CLIENT=$(gcloud iap oauth-clients list OAUTH_BRAND --filter="displayName:GKE Gateway controller by Terraform" --format="value(name)" | sed -E 's|.*/identityAwareProxyClients/(.*)$|\1|')
echo ${OAUTH_CLIENT}

OAUTH_BRANDの部分を置き換えてから実行してください。また、sed コマンドのバージョンにより、 -E オプションが使えない可能性があります。Cloud Shell では使えましたので、うまく表示できない方は Cloud Shell で試してください。

上手く行くと、echo コマンドで PROJECT_NUMBER-RANDOM_NUMBER.apps.googleusercontent.com のような形式の文字列が表示されます。この OAUTH_CLIENT 変数を使って、iap ディレクトリにある iap_policy.yaml ファイルを置き換えます。

sed -i "s/##CLIENT_ID##/${OAUTH_CLIENT}/" iap/iap_policy.yaml

コマンド実行後、 iap ディレクトリの iap_policy.yaml の内容を表示し、 clientID の部分が今回取得した OAuth Client ID に置き換わっていれば成功です。

OAuth Client Secret の作成

次に、OAuth Client Secret を取得して、その内容を iap-store-v1 という Secret オブジェクトに保存します。
前段で OAUTH_BRAND がある前提で、下記のコマンドを実行してください。

OAUTH_SECRET=$(gcloud iap oauth-clients list OAUTH_BRAND --filter="displayName:GKE Gateway controller by Terraform" --format="value(secret)")
kubectl create secret generic iap-store-v1 --from-literal=key=${OAUTH_SECRET}

これで、 iap-store-v1 という Secret が作成されました。念のため、下記のコマンドで作成されたことを確認します。

kubectl describe secret iap-store-v1

以下のように、 key: の部分が 35 bytes と表示されていれば大丈夫です。ここが 0 bytes になっていると、 kubectl create secret の引数に指定した OAUTH_SECRET 変数が空だったりするので、 kubectl delete secret iap-store-v1 で削除してからやり直してみてください。

Name:         iap-store-v1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
key:  35 bytes

検証

ようやく、検証用の設定を投入する準備ができました。
下記のコマンドでサンプル設定を投入することができます。

kubectl apply -f iap

IAP の設定は時間がかかるため、 15分くらい気長に待ちましょう。
ある程度の時間が経過したら、以下のコマンドを実行します。

curl -i https://gke-gateway-********.endpoints.PROJECT_ID.cloud.goog

以下のような結果になるはずです。レスポンスコードが302、location ヘッダに accounts.google.com 、レスポンスボディが Invalid IAP credentials: empty token になっていると思います。これは、IAP が動作し、認証トークンがないため Google アカウント認証のページにリダイレクトする動作になっています。

HTTP/2 302 
set-cookie: GCP_IAP_XSRF_NONCE_********; expires=Tue, 22-Aug-2023 11:14:29 GMT; path=/; Secure; HttpOnly
location: https://accounts.google.com/o/oauth2/v2/auth?client_id=********(省略)
x-goog-iap-generated-response: true
content-type: text/html; charset=UTF-8
content-length: 36
via: 1.1 google
date: Tue, 22 Aug 2023 11:04:29 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Invalid IAP credentials: empty token

今回のサンプル設定では、 store-v1 のみに IAP を設定しているため、 /v2/de といったパスにアクセスすると IAP が動作せずアプリケーションのレスポンスが確認できると思います。

設定のポイント

まず、 IAP の設定は GCPBackendPolicy オブジェクトで、 iap 属性を追加する必要があります。

https://github.com/cloud-ace/zenn-gke-gateway-policy-sample/blob/main/kubernetes2/iap/iap_policy.yaml

GKE Ingress controller における BackendConfig の IAP 設定と似ていますが、異なる点として以下が挙げられます。

  • OAuth Client ID は Secret オブジェクトに入れず、マニフェストの clientID に直接設定する。
  • OAuth Client Secret のみを Secret オブジェクトに入れる必要がある。そのときの Key 名は、実は key じゃなくてもよい。

混乱しないように注意しましょう。私は割と混乱しました。
何故、 BackendConfig から仕様を変えたのか、コレガワカラナイ

クリーンアップ

検証が終わったら、以下のコマンドで設定を掃除してください。

kubectl delete -f rewrite

検証環境クリーンアップ

前回ブログ記事と同様、検証完了しましたらリソースをクリーンアップします。注意事項(一部リソースの残存)についても前回同様です。

Kubernetes オブジェクトの削除

kubectl delete -f base

Google Cloud リソースの削除

cd ../terraform
terraform destroy

まとめ

2023年7月10日に追加された GKE Gateway controller の機能について紹介しました。
IAP は Ingress のときから設定仕様が少し変わってしまいましたが、分かってしまえばそんなにややこしくはないと思います。
今回の機能で、GKE Ingress controller で実装されていた Cloud Load Balancing の機能は Gateway controller にも実装され、残るは Cloud CDN くらいだと思います。今後も Gateway controller の最新状況を紹介できればと思います。

Discussion