TT Lab
はじめる
学ぶ 学習パス コース

CCA — Cilium認定アソシエイト

Gateway APIを有効にしたのにGatewayClassがない

TT Labで続きを見る

目標

VM内の本物のk3s + Cilium 1.20.1で、Gateway APIを間違った順序で有効にして、それを正したあと、Gateway・HTTPRouteで、パス・ヘッダー・重みによる分割と、ネームスペースの境界(ReferenceGrant)を、実際のリクエストで確認します。最後に、リクエストを実際に受けているのはノードのcilium-envoyであることを、Serviceの一覧から見つけ出します。

なぜ重要なのか

Ingressは、パスとホスト程度だけを標準で定め、残りを実装ごとのアノテーションに任せていました。Gateway APIは役割を分けます。インフラ担当はGatewayClassとGatewayを、アプリチームはHTTPRouteを所有し、ヘッダーマッチングや重みによる分割のような機能がスペックの中に入っていて、別のネームスペースを参照するには、受ける側がReferenceGrantで許可しなければなりません。

Ciliumは、サイドカーなしでこのAPIを実装します。L7の処理は、ノードごとに1つずつ動くenvoyが担当し、eBPFが、ゲートウェイのアドレスに来たトラフィックをそのenvoyに渡します。この構造を知らないと、「ゲートウェイのPodはどこにあるのか」を探して時間を使ってしまいます。

そして、有効にする順序が重要です。公式ドキュメントは、CRDを先にインストールするよう述べています。このラボで順序を逆にすると、何が空になるのか、再起動だけではなぜ埋まらないのかを、自分で確認します。

環境の準備に約5分かかります。Gateway API CRDは、github rawから取得します。セッションが終わると、/root/cca-gatewayのファイルは消えます。

ステップ

  1. cilium CLIでGateway API機能を有効にしてください(cilium upgrade --version 1.20.1 --reuse-values --set gatewayAPI.enabled=true)。そのあと、公式ドキュメントの順序どおりに、cilium-operator Deploymentとcilium DaemonSetを再起動します。Gateway API CRDはまだインストールしません。その瞬間を、/root/cca-gateway/no-crd.jsonに記録します。記録する項目は、enable_gateway_api(cilium-configのenable-gateway-apiの文字列)、gatewayclass_api(APIにgatewayclassesリソースがあればtrue、なければfalse)、operator_log(新しいoperatorが、CRDがないと残したログ1行をそのまま)です。
  2. Gateway API v1.6.1の標準CRD 7個(gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes)を、kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.6.1/config/crd/standard/gateway.networking.k8s.io_<자원>.yaml(プレースホルダーはリソース名です)でインストールしてください。そのあと、cilium-operatorをもう一度再起動し、新しいoperator Podが起動したあと20秒以上待ってから、GatewayClassの数を数えます。/root/cca-gateway/crd-late.jsonに、crds(インストールされたCRDの完全な名前7個のリスト)、operator_started(再起動したoperator Podのstatus.startTime)、gatewayclasses(そのときの数)、recorded_at(数えた瞬間のUTC時刻、date -u +%Y-%m-%dT%H:%M:%SZ)を記録します。Ciliumの設定は、まだ再適用しません。
  3. CRDがある状態で、ステップ1のcilium upgradeコマンドをもう一度実行して、GatewayClass ciliumが作成され、Accepted=Trueになるようにしてください。/root/cca-gateway/gatewayclass.jsonに、name、controller(spec.controllerName)、accepted(Accepted conditionのstatus)、managed_by(ラベルapp.kubernetes.io/managed-byの値)を記録します。
  4. kubectl apply -f /opt/fixtures/cca-gateway/backends.jsonで、バックエンド(cca-gwのstore-v1・store-v2・admin、cca-shopのpayments)を起動してください。cca-gwネームスペースにGateway shop-gwを作成します。gatewayClassNameはcilium、リスナーは1つ(nameはhttp、protocolはHTTP、portは80)です。Programmed=Trueになったら、/root/cca-gateway/gateway.jsonに、address(status.addressesの値)、programmed、service(自動生成されたService名)、service_type、proxy_backend(agentのcilium-dbg service listで、そのアドレス:80のLoadBalancerフロントエンドのバックエンドip:port)、root_code(ノードからcurl http://<주소>/(プレースホルダーはアドレスです)を実行したときのHTTPコード、数値)、server(その応答のserverヘッダーの値)を記録します。
  5. cca-gwにHTTPRoute storeを作成してください。parentRefsはshop-gwの1つ、ルールは2つです。(1) path PathPrefixが/storeで、かつヘッダーx-canary: yesのリクエストはstore-v2:8080、(2) path PathPrefixが/storeの残りはstore-v1:8080です。ノードからゲートウェイのアドレスに、/store/list、ヘッダーを付けた/store/list、/storefrontをリクエストし、/root/cca-gateway/routes.jsonに、store(1つ目のリクエストの応答JSONのapp)、store_canary(2つ目のapp)、storefront(3つ目のHTTPコード、数値)を記録します。
  6. cca-gwにHTTPRoute splitを作成してください。parentRefsはshop-gw、ルールは1つで、path PathPrefixは/checkout、backendRefsはstore-v1:8080のweight 80と、store-v2:8080のweight 20です。ルートが反映されたあと、ゲートウェイのアドレスの/checkoutに100回リクエストして、応答のappを数え、/root/cca-gateway/split.jsonに、path、requests(送信した数)、counts({store-v1: n, store-v2: m})を記録します。両方のバージョンが観測される必要があります。
  7. cca-gwにHTTPRoute payを作成してください。parentRefsはshop-gw、path PathPrefixは/pay、backendRefsは、別のネームスペースにあるpayments:8080(namespaceはcca-shop)です。まず、許可なしで適用して、routeのResolvedRefs condition(status・reason)と、/payのHTTPコードを観測します。次に、cca-shopにReferenceGrantを作成して、cca-gwのHTTPRouteがService paymentsだけを参照できるように許可し、もう一度観測します。/root/cca-gateway/refgrant.jsonに、beforeとafterのそれぞれについて、resolved_refs、reason、code(数値)を記録します。
  8. /root/cca-gateway/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を7行書きます。項目は、gatewayclass_owner(GatewayClass ciliumのmanaged-byラベル)、l7_proxy_daemonset(kube-systemで、ゲートウェイのリクエストを実際に処理しているDaemonSet名)、gateway_frontend(アドレス:80)、gateway_proxy_backend(ステップ4と同じ方法で、現在読み取ったバックエンド)、envoy_config(cca-gwに自動生成されたCiliumEnvoyConfig名)、split_v2_count(split.jsonのstore-v2の数)、cross_namespace_without_grant(許可がないときのHTTPコード)です。値は、実際の状態および記録と一致している必要があります。

参考

CRDなしでゲートウェイ機能から有効にした日

cilium CLIでGateway API機能を有効にしてください(cilium upgrade --version 1.20.1 --reuse-values --set gatewayAPI.enabled=true)。そのあと、公式ドキュメントの順序どおりに、cilium-operator Deploymentとcilium DaemonSetを再起動します。Gateway API CRDはまだインストールしません。その瞬間を、/root/cca-gateway/no-crd.jsonに記録します。記録する項目は、enable_gateway_api(cilium-configのenable-gateway-apiの文字列)、gatewayclass_api(APIにgatewayclassesリソースがあればtrue、なければfalse)、operator_log(新しいoperatorが、CRDがないと残したログ1行をそのまま)です。

機能のスイッチはConfigMapを変えるだけで、operatorは起動時にGateway APIのリソースがあるかを点検します。再起動したoperatorのログから、level=errorの行を探してみてください。リソースの有無は、kubectl api-resources --api-group=gateway.networking.k8s.ioで確認します。ファイルをjq -nの--arg/--argjsonで作成すると、引用符が混ざったログ行も安全に格納できます。

CRDをあとからインストールして再起動したのに、空のままだ

Gateway API v1.6.1の標準CRD 7個(gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes)を、kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.6.1/config/crd/standard/gateway.networking.k8s.io_<자원>.yaml(プレースホルダーはリソース名です)でインストールしてください。そのあと、cilium-operatorをもう一度再起動し、新しいoperator Podが起動したあと20秒以上待ってから、GatewayClassの数を数えます。/root/cca-gateway/crd-late.jsonに、crds(インストールされたCRDの完全な名前7個のリスト)、operator_started(再起動したoperator Podのstatus.startTime)、gatewayclasses(そのときの数)、recorded_at(数えた瞬間のUTC時刻、date -u +%Y-%m-%dT%H:%M:%SZ)を記録します。Ciliumの設定は、まだ再適用しません。

operatorがCRDを認識することと、GatewayClassオブジェクトが作成されることは、同じこととは限りません。数が0なら、そのGatewayClassを本来誰が作るのかが気になってくるはずです。次のステップで、ラベルで確認します。Podが2つ見える場合は、creationTimestampが最も新しいものが新しいPodです。採点ツールは、ファイルの更新時刻ではなくrecorded_atを、再起動の時刻、およびあとで作成されるGatewayClassの作成時刻と比較するので、記録した瞬間の時刻を書いてください。

GatewayClassはチャートが作る

CRDがある状態で、ステップ1のcilium upgradeコマンドをもう一度実行して、GatewayClass ciliumが作成され、Accepted=Trueになるようにしてください。/root/cca-gateway/gatewayclass.jsonに、name、controller(spec.controllerName)、accepted(Accepted conditionのstatus)、managed_by(ラベルapp.kubernetes.io/managed-byの値)を記録します。

GatewayClassのラベルとアノテーションに、誰がこのオブジェクトを所有しているのかが書かれています。Helmチャートは、レンダリングするときに、クラスターにどんなAPIがあるかを見て、テンプレートを含めたり除いたりできます。conditionがTrueになるまで、数秒間隔で読み直してください。

ゲートウェイのアドレスの背後に立っているのはenvoyだ

kubectl apply -f /opt/fixtures/cca-gateway/backends.jsonで、バックエンド(cca-gwのstore-v1・store-v2・admin、cca-shopのpayments)を起動してください。cca-gwネームスペースにGateway shop-gwを作成します。gatewayClassNameはcilium、リスナーは1つ(nameはhttp、protocolはHTTP、portは80)です。Programmed=Trueになったら、/root/cca-gateway/gateway.jsonに、address(status.addressesの値)、programmed、service(自動生成されたService名)、service_type、proxy_backend(agentのcilium-dbg service listで、そのアドレス:80のLoadBalancerフロントエンドのバックエンドip:port)、root_code(ノードからcurl http://<주소>/(プレースホルダーはアドレスです)を実行したときのHTTPコード、数値)、server(その応答のserverヘッダーの値)を記録します。

Gatewayを作成すると、コントローラーがLoadBalancer Serviceを作成し、このk3sではservicelbがノードのIPを割り当てます。そのServiceのフロントエンドを、agentのServiceリスト(-o json)から探して、バックエンドがPodのIPなのか、ノードローカルのアドレスなのかを見てください。パスのルールが1つもないとき、誰がどんなコードで応答するのかも確認します。アドレスが付いた直後は、接続が一時的に失敗することがあるので、ポーリングしてください。

パスが同じでも、ヘッダーが違えば別のバージョンへ

cca-gwにHTTPRoute storeを作成してください。parentRefsはshop-gwの1つ、ルールは2つです。(1) path PathPrefixが/storeで、かつヘッダーx-canary: yesのリクエストはstore-v2:8080、(2) path PathPrefixが/storeの残りはstore-v1:8080です。ノードからゲートウェイのアドレスに、/store/list、ヘッダーを付けた/store/list、/storefrontをリクエストし、/root/cca-gateway/routes.jsonに、store(1つ目のリクエストの応答JSONのapp)、store_canary(2つ目のapp)、storefront(3つ目のHTTPコード、数値)を記録します。

1つのルールのmatches項目の1つの中に、pathとheadersを一緒に置くと、両方を満たす必要があります。より具体的な条件(ヘッダーがあるほう)が優先されるように、Gateway APIが定めています。PathPrefixは、文字単位ではなく、/で区切られたパス要素の単位で比較します。バックエンドは、自分の名前をJSONで返します。

20%だけ新しいバージョンに流すという約束を数えてみる

cca-gwにHTTPRoute splitを作成してください。parentRefsはshop-gw、ルールは1つで、path PathPrefixは/checkout、backendRefsはstore-v1:8080のweight 80と、store-v2:8080のweight 20です。ルートが反映されたあと、ゲートウェイのアドレスの/checkoutに100回リクエストして、応答のappを数え、/root/cca-gateway/split.jsonに、path、requests(送信した数)、counts({store-v1: n, store-v2: m})を記録します。両方のバージョンが観測される必要があります。

重みは、リクエストを1件ずつ比率に基づいて振り分けるものであり、ちょうど5回に1回送るルールではありません。そのため、サンプルの数は80/20付近で揺れます。ルートを作成した直後は、envoyの設定が広がる間に404が混ざることがあるので、2つのバージョンが1回ずつ見えたあとで、数え始めてください。採点ツールは、記録を重みと突き合わせ、自分でもサンプルを取り直します。

隣のネームスペースの決済Serviceは、許可を受けないと接続できない

cca-gwにHTTPRoute payを作成してください。parentRefsはshop-gw、path PathPrefixは/pay、backendRefsは、別のネームスペースにあるpayments:8080(namespaceはcca-shop)です。まず、許可なしで適用して、routeのResolvedRefs condition(status・reason)と、/payのHTTPコードを観測します。次に、cca-shopにReferenceGrantを作成して、cca-gwのHTTPRouteがService paymentsだけを参照できるように許可し、もう一度観測します。/root/cca-gateway/refgrant.jsonに、beforeとafterのそれぞれについて、resolved_refs、reason、code(数値)を記録します。

ReferenceGrantは、参照を受ける側のネームスペースに置きます。fromには、参照するリソースのgroup・kind・namespaceを、toには、参照されるリソースのgroup・kind(そして絞り込むならname)を書きます。コアAPIのgroupは、空文字列です。conditionは、status.parents[0].conditionsからtypeで選んで読んでください。

開通レポート: 誰が何を作り、誰がリクエストを受けたのか

/root/cca-gateway/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を7行書きます。項目は、gatewayclass_owner(GatewayClass ciliumのmanaged-byラベル)、l7_proxy_daemonset(kube-systemで、ゲートウェイのリクエストを実際に処理しているDaemonSet名)、gateway_frontend(アドレス:80)、gateway_proxy_backend(ステップ4と同じ方法で、現在読み取ったバックエンド)、envoy_config(cca-gwに自動生成されたCiliumEnvoyConfig名)、split_v2_count(split.jsonのstore-v2の数)、cross_namespace_without_grant(許可がないときのHTTPコード)です。値は、実際の状態および記録と一致している必要があります。

サイドカーのないCiliumでは、L7はノードごとに1つずつ動くenvoyが担当します。kubectl -n kube-system get dsとkubectl get ciliumenvoyconfig -Aで、ゲートウェイが残した痕跡を探し、Serviceリストの127.0.0.1のバックエンドと結びつけてみてください。