GKEとExternal HTTP(S) Load BalancingでPath rewriteを実現する

Kakeru Ishii
google-cloud-jp
Published in
21 min readSep 13, 2021

Google Cloudの Kakeru です。GKE の Ingress を設定する際にPath rewrite したいことってありますよね。今回はその方法を説明します!

LB で Path rewriteしたい

後方互換性をどうしても保たなければならず、新しいAPIをURLでバージョンを変えてデプロイしたいケースを考えます。こんなとき、例えば API サーバは以下のような URL を持つことでしょう。

  • https://<ドメイン>/api/v1/hoge/fuga
  • https://<ドメイン>/api/v2/hoge/fuga

中身の実体としてはバージョンの異なる、別々のサーバプログラムなので、 プログラム本体が/api/v1/ などのバージョン情報をパスから受け取る必要はありません。むしろ、 /hoge/fuga のパスにアクセスされたと認識するような処理が共通化されていれば、バージョンが異なっていても開発側は意識する必要がなく、サーバプログラムをデプロイする運用管理者が外から見えるバージョンを管理することができるようになります。

例えば、nginxのrewriteやAppacheのmod_rewriteなどによってこの書き換えが可能です。

しかし、GCP を使う上では様々な機能と連携できる External HTTP(S) Load Balancing を用いて、 ロードバランサ上で追加コンポーネントをインストール・設定することなく Path を rewrite して欲しいですよね。(図1)

図1. path の rewrite機能が欲しい例

この例以外にも、Path を rewrite したいケースがあります。

  • 一つのホスト名でパスごとに複数のサーバプログラムにルーティングしたいケース
  • 既存のアプリケーションをホストしていて、サーバプログラム側の修正の必要なく外から見たパスを変更したい時

このような、 Pathのrewrite、色々な場面で欲しい機能ですが、 External HTTP(S) Load Balancing を GKEのIngressから構成する場合には設定することができません。(詳細は後述)

この記事では、GKE の Standalone NEGと手動で構成したExternal HTTP(S) Load Balancingを設置してこの機能を実現します。また、Config Connector を利用してこのような設定を k8s に即した手法で Standalone NEGと協調する External HTTP(S) Load Balancingを構成する手法を説明します。

実験用の GKE クラスタを作成する

実際に External HTTP(S) Load Balancing を用いて Path rewrite を行うため、実験用に GKE のクラスタを立てます。

今回は以下の機能を使うので、クラスタ作成時に有効にしてください。
(図2–4)

  • VPC-native traffic routing
  • Workload Identity
  • Config Connector
図2. SecurityからEnable Workload Identityにチェックされてることを確認
図3. NetworkingからEnable VPC-native traffic routingにチェックされてることを確認
図4. FeaturesからEnable Config Connectorがチェックされていることを確認

また、Config Connector経由で GCP の APIを叩くため、各ノードは十分なアクセススコープが必要です。(図5)
今回は実験用のクラスタですので、全てのアクセスを許可したスコープを設定しました。プロダクション環境等での利用では、動作させるアプリケーションに必要な最小限のアクセススコープを適用してください。

図5. node-poolsオプションのSecurityでAccess ScopeがAllow full access…になっていることを確認

実験用のサーバプログラムのコンテナイメージを作成する

今回はHostヘッダとPathを表示するサーバを書きます。

main.py : リクエスト先のホスト名とパスを表示するサーバ

この例では、Poetry を用いて依存関係を管理しているため、 poetry run python main.py で動作します。

このプログラムを動作させると、サーバプログラムは8000番ポートを Listen して待機します。

curl コマンドを用いてアクセスしてみましょう。

$ curl http://localhost:8000/hoge/fuga
localhost:8000/hoge/fuga

のように表示されるはずです。

今回はこのパスを External HTTP(S) Load Balancing を用いて rewrite します。

作成したコンテナをDeploymentとしてクラスタにデプロイします。

通常の Ingress の挙動を確認してみる

ここで実際に rewrite ができる構成を試す前に、 通常の手法で Ingress をデプロイしてみます。

こうすることによって、 https://<LBのIP>/api/v1/XXXX/YYYYhttps://<LBのIP>/api/v2/XXXX/YYYY でデプロイされたPodにアクセスすることができます。

(ここでは簡略化のため、v1,v2共に接続先が同じ Service になっていますが、現実的にはそれぞれ違う Service を指定します。また、その場合でもbackend の name を変えるだけで動作します。)

これではhttps://<LBのIP>/api/v1/XXXX/YYYY にアクセスした際に<LBのIP>/api/v1/XXXX/YYYY と表示されることになります。こちらの Ingress の設定から path rewrite の設定が出来ても良さそうですが、KubernetesのIngressの標準仕様にはRewriteに関する定義がなく、Rewriteの設定はIngress 以外の方法で記述する必要があります。

Standalone NEGとExternal HTTP(S) Load Balancing で Path を rewrite したい

External HTTP(S) Load Balancing は Path の rewrite を2年ほど前からサポートしています。

Standalone NEG を用いて手動でExternal HTTP(S) Load Balancingを構成する

このクラスタはVPC nativeなネットワークが有効として構成されているため、先程デプロイしたサービスには cloud.google.com/neg というannotationが追加されています。(図6)
これにより、 Network Endpoint Group(NEG) というリソースが作成されます。このNEGを介して手動で構成した ロードバランサと k8s 上の Service を繋ぐことができ、External HTTP(S) Load Balancingで本来使える機能が使えるようになります。

図6. ServiceのDetailsを見るとcloud.google.com/negが追加されている

単に Ingress を作成すれば良い時には、 NEG の存在を意識する必要性はあまりありません。 下記の図の Ingress with NEGs の部分のように、Ingress Controllerは Ingress が作成されると内部的に様々なリソースを作成します。そして、 Ingress Controllerが作成する、最もPodに近い側のリソースとして Backend Serviceがありますが、こちらが Service 作成時に NEG Controller によって作成された NEG と通信します。そのため、 Ingress によって Service の名前を指定してロードバランサを設定している際には、多くの場合 NEG の存在を意識する必要性がありません。

図7. Standalone NEGの管理する範囲 :引用元 https://cloud.google.com/kubernetes-engine/docs/how-to/standalone-neg

一方、今回は Ingress が普段自動で作ってくれるリソースの中に存在するURL Map をカスタマイズすることで path の rewrite を実現します。そのため、 Service の作ってくれる NEG をそのまま利用し、普段 Ingress Controller が作ってくれる部分を自分で作成する必要があります。このようなユースケースにおける NEG をStandalone NEG と呼んでいます。

自動で追加された cloud.google.com/neg によって追加された NEG はランダムなID を持つ名前で追加されています。名前はデプロイ時にわかっていてくれないと不便なので、Service の設定を以下のように server-negという名前に修正します。(28行目)

実際にこちらをデプロイすると、 Cloud Console から server-neg という NEG が追加されているのを見ることができます。(図8)

図8.「Compute Engine」-> 「Network endpoint groups」からServiceのために作成されたNEGを見ることができる

普段 Ingress によって作成、管理されているリソースをまずは手動で作成してみます。(後ほど、Config Connectorを用いてコードで管理できる形にする話をします)

図9. k8s の Serviceと共に作成される部分と、これから手動で作る部分

具体的には以下が必要です。(図9)

  • Firewall Rule (HealthCheck が HTTPリクエストをサービスに届けるためにFWで弾かないために必要)
  • BackendService
  • HealthCheck (BackendService はヘルスチェック状態を監視し、HealthyなNEGのエンドポイントに対してリクエストをルートするため)
  • 外部IPアドレス
  • URL Map(今回カスタマイズする部分)
  • Target HTTP Proxy
  • Forwarding Rule

今回はForwarding Ruleを設定する際に、自動で生成される外部IPアドレスを利用します。今回は実験用なのでエフェメラルなIPアドレスで問題ないため、明示的に外部IPアドレスは作成しません。

Firewall ruleの作成

Backend Service に紐づくヘルスチェックでは、 NEG の指定先のエンドポイントに対して130.211.0.0/22 もしくは 35.191.0.0/16 の範囲からリクエストを飛ばして、対象のエンドポイントが生きているか調べます。(図10)この接続が弾かれないよう、Firewall Ruleを構成する必要があります。

図10. この節で作成する Firewall の部分

以下のコマンドでこのFirewall Ruleを作成することが可能です。

$ gcloud compute firewall-rules create fw-allow-gclb-healthcheck \
--network default \
--action allow \
--direction ingress \
--source-ranges 130.211.0.0/22,35.191.0.0/16 \
--rules=tcp:8000

このヘルスチェックがアクセスするのは、 Pod そのものであるため、Service を通過する前の Port 番号であることも注意が必要です。今回の例では Service は80番ポートでアクセスを待ってますが、各Podは8000番ポートでアクセスを待っています。ヘルスチェックをする際には、サービスの接続先( NEG に含まれているエンドポイント = Pod) にアクセスするため、許可すべきなのは Pod 側に指定されているポート番号になります。

HealthCheck、BackendServiceの作成

Health Check(gclb-test-health-check)、Backned Service(bes-gclb-test) を作成します(図11)。

図11. この節で作成する Health Check と Backend Service
$ gcloud compute health-checks create http gclb-test-health-check \
--use-serving-port
$ gcloud compute backend-services create bes-gclb-test \
--protocol HTTP \
--health-checks gclb-test-health-check \
--global

Backend Serviceはどの NEG にルーティングするのかまだ設定していないため、動作しませんが、これについては最後に追加を行います。

URL Mapの作成、カスタマイズ

図12. この節で作成する URL Map

ここで、本題の URL Mapを作成します。(図12)
まず、あらかじめURL Map用の設定ファイルを作成します。以下の PROJECT_ID をご自身のお使いの GCP のプロジェクトIDに差し替えてください。

ここでは、pathMatcher内のpathRulesに対して2つのルールを作成します。一つは、 /api/v1 及び、 /api/v1/* にマッチし、2つ目は /api/v2 及び /api/v2/* にマッチします。

⚠️/api/v1/* のみの指定をすると、 /api/v1 にはマッチしないので注意してください。

各 paths の中に指定する routeAction では、prefix部分をどのようなパスに置き換えるか指定します。例えば、ここに今回の例のように / を指定した場合には、 http://<LBのIP>/api/v1/hoge/fugaへのアクセスはPod側ではhttp://<LBのIP>/hoge/fuga として rewriteされた状態でルーティングされます。

一方、例えば /foo/bar と指定した場合には、 http://<LBのIP>/api/v1/hoge/fuga へのアクセスは Pod 側では http://<LBのIP>/foo/bar/hoge/fuga としてrewriteされた状態でルーティングされます。

作成した URL Mapのコンフィグが正しいかどうかは gcloud compute url-maps validate を用いて検証します。

$ gcloud compute url-maps validate --source ./url-map.yaml
result:
loadSucceeded: true
testPassed: true

URL Mapの設定に問題がないと確認ができたら、こちらのURL Mapを以下のコマンドを用いて適用します。

$  gcloud compute url-maps import gclb-test-url-map \
--source ./url-map.yaml

これによって、 pathの rewrite を設定した URL Map(gclb-test-url-map) を作成することができました。

Target HTTP Proxy、Forwarding Ruleの作成

Target HTTP Proxy(gclb-test-lb-proxy)とForwarding Rule(gclb-test-forwarding-rule)を作成します(図13)。

図13. Forwarding Rule と Target HTTP Proxyの作成。 外部IPはForwarding Ruleから自動で割り当てられる

Forwarding Rule を作成することによって、Cloud Console上から Load balancer として確認することができるようになります。今回、Forwarding Rule には外部 IP アドレスを指定していないため、エフェメラルな IP が自動的に割り当てられます。

以下のコマンドを用いて作成を行います。

$ gcloud compute target-http-proxies create gclb-test-lb-proxy \
--url-map gclb-test-url-map
$ gcloud compute forwarding-rules create gclb-test-forwarding-rule \
--global \
--target-http-proxy gclb-test-lb-proxy \
--ports=80

Backend ServiceにNEGを追加する

最後に Backend Service に backendとして NEG を追加します。(図14)

図14. 最後にBackend Serviceと NEG を繋ぐ
$ gcloud compute backend-services add-backend bes-gclb-test \
--global \
--network-endpoint-group server-neg \
--network-endpoint-group-zone asia-northeast1-a \
--balancing-mode RATE \
--max-rate-per-endpoint 5

作成直後は404等が表示される可能性がありますが、数十秒から数分程度で表示されるようになります。アクセスしてみると、確かに rewrite できていることがわかります。(図15)

図15. rewriteすることができた!

Config Connectorを用いて半自動化する

さて、設定することはできましたが、コマンドの数も多く、設定するたびにこれを弄っていたらバグを引き起こしかねません。Terraformを用いるのは一つの手ですが、今まで Ingress で管理できていたものをk8sの外で別の仕組みで管理し始めるのは微妙なところです。

そこで、 Config Connector を用いてこちらの設定を半自動化します。

Config Connectorはクラスタ作成時のオプションで有効化しただけでは利用可能にはなりません。IDの設定と、Config Connector用の設定が必要になります。

Config Connectorの初期設定については本記事では扱いません。以下の公式ドキュメントや、過去のブログ記事を参考に設定を行ってください。

先程のコマンドを全て Config Connector用のCRDの形式に合わせてYAMLファイルを記述します。Backnend Serviceの設定中の PROJECT_ID , NEG_ZONEを書き換える必要があることに注意してください。

Backend Serviceの記述の際には、 networkEndpointRef にNEGの参照を指定する必要があります。 NEG もConfig Connector を用いて作成されている場合には、 networkEndpointRef.name にk8s上でのリソース名を指定します。

しかし、今回は Service によって生成された NEG であるため、完全修飾名を networkEndpointRef.external に指定します。こちらの完全修飾名は、NEGのServiceによって作成されたNEGのページから EQUIVALENT REST をクリックすると selfLink の項目から確認することができます。(図16)

図16. NEGの完全修飾名の確認の方法: Compute Engine -> Network endpoint groups -> Serviceによって作られたのNEGのページ

これらのYAMLファイルを kubectl applyすることによって、Config ConnectorによってGCP上のリソースとしてデプロイされます。

今回の例では、最小限の設定でロードバランサを設定しました。Standalone NEGを用いることで、Ingress を用いず直接 External HTTP(S) Load Balancing を設定できるので、Cloud Armor との組み合わせや、 複数クラスタにルーティング等、様々なことが可能です。

さて、ロードバランサに割り当てられた外部IPアドレスは、kubectl describeでIPアドレスように作成したリソースの詳細を表示することが可能です。(もしくは単に Cloud Consoleの外部IPアドレスから確認できます)

$ kubectl describe computeaddress.compute.cnrm.cloud.google.com/gclb-test-external-ip  | grep Address

こうして得られた IP アドレスに対して リクエストを行うことで、先程コマンドで指定したものと同じようにアクセスができるということがわかります。(図17)

図17. 同様にアクセスできることがわかる

最後に

いかがでしたか。Standalone NEGを活用することによって、今回の目的の Path の rewriteのみならず、より自由度の高いロードバランサの構成が可能になります。
今回は External HTTP(S) Load Balancing を手動で構成するのに複数のコマンドが必要だったことから、IaCの一環として Config Connector での設定方法を紹介しました。GKE に関連してクラスタ上にないリソースを管理するためには最適な手段でしょう。
また、 Ingress を直接的に書けないながらも、 k8s のリソースとしては管理することができ、比較的今までのワークフローと近い形で path の rewrite を構成できたのではないでしょうか。

--

--