GKE Gateway controller の新機能を試してみた カスタムヘッダー, URL リライト, IAP 編
こんにちは。クラウドエースの阿部です。
こちらのブログ記事では、前回のブログ記事に引き続き、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) って何?という方は、下記のブログ記事もご一読ください。
環境準備
OAuth 同意画面の構成
今回のブログ記事では、 IAP を使った検証を行います。IAP を使用するためには、最初に OAuth 同意画面を構成する必要があります。
下記のドキュメントに記載されている「OAuth 同意画面の構成」の手順に従い、設定を行ってください。(これを設定しないと、 Terraform で OAuth Client を作成する際に失敗する場合があります)
なお、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=XXXXXX
の XXXXXX
箇所は、使用可能なプロジェクト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"
以下のような図のリソースが一通り作成されます。
主なリソースは前回のブログ記事で構築したリソースと同じですが、よく見ると google_iap_client
というリソースが追加になっていることが分かると思います。
共通で使用する Kubernetes オブジェクト
GKE クラスタ作成後、 kubectl
コマンドを使って Kubernetes オブジェクトをデプロイします。
今回は、 kubernetes2
というディレクトリにある YAML ファイルを使用します。 ※/kubernetes
は前回ブログ記事のサンプルコードディレクトリです。
cd ../kubernetes2
上記ディレクトリに移動後、 base
にある設定を投入します。
kubectl apply -f base
作成される Kubernetes オブジェクトは前回ブログ記事と同等です。異なる点として、 store-v1
、store-v2
、store-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-Location
、X-Client-Ip-Address
、X-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-rtt
とx-version
ヘッダが付与されていることがわかります。また、 server
ヘッダが Werkzeug フレームワークを示すものから、anonymous という匿名を示す文字列に変更されています。
設定のポイント
カスタムリクエストヘッダの設定のポイントは以下の箇所です。
matches
と同じインデント位置に filters
属性を付与して、 type: RequestHeaderModifier
を設定しています。この設定で、カスタムリクエストヘッダを実現しています。
また、 X-Client-IP-Address
と X-Client-Geo-Location
は固定値ではなく {client_ip_address}
のようなブレースで変数を設定しています。この変数はKubernetes Gateway API ではなくアプリケーションロードバランサの機能で、こちらのドキュメントに設定可能な変数が掲載されています。
同様に、カスタムレスポンスヘッダの設定のポイントは以下の箇所です。
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 -
設定のポイント
パスリダイレクトの設定は以下の箇所です。filters
に type: RequestRedirect
を設定し、何のステータスコードでどのパスにリダイレクトするかを設定します。
また、URL リライトの設定は以下の箇所です。filters
に type: URLRewrite
を設定し、hostname と 書き換えるパスを指定すればよいです。
クリーンアップ
次の検証に移る前に、以下のコマンドで設定を掃除してください。
kubectl delete -f rewrite
Identity-aware Proxy (IAP)
IAP は、ロードバランサのバックエンドサービスに認可プロキシを追加することで、簡単に Google アカウントによるアクセス制御を追加することが可能なサービスです。詳細は下記のドキュメントを参照してください。
検証の準備
これまでと異なり、パラメータを編集して作成する作業が多いため、間違えないようにご注意ください。
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 属性を追加する必要があります。
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