門を二つ開けたら見張りも二人立てる
目標
本物のIstioメッシュで、Istio GatewayとGateway APIの2つの方式でIngressを開き、TLS終端と重みによる振り分け、ゲートウェイの認可ポリシーが実際にどこにかかるかを、レスポンスとEnvoyの設定で確認します。
なぜ重要なのか
Ingressはメッシュの最初の関門なので、ここで間違えると外からすぐ見えます。ところが、IstioとGateway APIが併用される現在は、同じサービスに入り口が2つできやすく、2つの入り口は別々のPodなので、証明書もポリシーも別々です。マニフェストが「適用された」というだけではこの違いが見えないので、実際のリクエストと、ゲートウェイのEnvoyが受け取った設定で確認します。
ステップ
kubectl apply -f /opt/fixtures/istlab/ingress-app.yamlでネームスペースmallのweb-v1・web-v2を載せ、2つのDeploymentの準備ができるまで待ってください。その後、/root/istlab-ingress/01-gateway.txtに、Ingressゲートウェイのingress_cluster_ip=(Serviceistio-system/istio-ingressgatewayのClusterIP)とingress_pod=(そのゲートウェイのPod名)の2行を書いてください。/root/istlab-ingress/gateway.yamlに次の2つのリソースを書いて適用してください。ネームスペースmallに、IstioのGatewayであるmall-gw(セレクターistio: ingressgateway、ポート80のHTTP、ホストmall.example.com)と、VirtualServiceのmall-ingress(ホストmall.example.com、ゲートウェイmall-gw、宛先web-v1.mall.svc.cluster.localのポート80)です。適用後に、Host: mall.example.comでゲートウェイのClusterIPを呼ぶと、v1が返る必要があります。opensslでCN=mall.example.com、SANDNS:mall.example.comの自己署名証明書と鍵を、/root/istlab-ingress/tls/mall.crt・/root/istlab-ingress/tls/mall.keyとして作り、istio-systemにTLS Secretのmall-credとして入れてください。その後、/root/istlab-ingress/gateway.yamlのGatewayに、ポート443のHTTPSサーバー(ホストmall.example.com、tls.mode: SIMPLE、credentialName: mall-cred)を追加して、もう一度適用します。curl --cacert /root/istlab-ingress/tls/mall.crt --resolve mall.example.com:443:<ClusterIP> https://mall.example.com/がv1を返す必要があります。/root/istlab-ingress/gwapi.yamlに次の2つのリソースを書いて適用してください。ネームスペースmallのConfigMapのmall-api-options(data.serviceにspec: {type: ClusterIP})と、Gateway APIのGatewayであるmall-api(gatewayClassName: istio、infrastructure.parametersRefでそのConfigMap、リスナーhttpはポート80のHTTP、ホスト名api.mall.example.com、同じネームスペースのルートだけを許可)です。GatewayがProgrammedになるまで待ってください。/root/istlab-ingress/httproute.yamlにHTTPRouteのapi-splitを書いて適用してください。親はGatewayのmall-api、ホスト名はapi.mall.example.com、ルールは2つです。ヘッダーx-canary: yesならweb-v2(ポート80)へ、それ以外はweb-v1の80とweb-v2の20の重みで振り分けます。mall-api-istioのServiceのClusterIPにx-canary: yesのリクエストを送ると、常にv2が返る必要があります。/root/istlab-ingress/06-compare.txtに4行を書いてください。Istio Gatewayが選んだゲートウェイPodのネームスペースistio_gateway_ns=、Gateway APIが作ったゲートウェイPodのネームスペースgwapi_gateway_ns=、そのDeploymentの名前gwapi_deployment=、そのServiceの種類gwapi_service_type=です。/root/istlab-ingress/deny-admin.yamlに、istio-systemのAuthorizationPolicyであるdeny-adminを書いて適用してください。セレクターはistio: ingressgateway、action: DENY、パスは/adminと/admin/*です。適用後、/root/istlab-ingress/07-scope.txtに、istio_gateway_admin=(mall.example.com/adminのステータスコード)とgwapi_admin=(api.mall.example.com/adminをGateway APIのゲートウェイに送ったステータスコード)の2行を書いてください。/root/istlab-ingress/08-report.mdに5行を書き、その下に学んだことを4行以上書いてください。5行は、tls_fingerprint_match=(ゲートウェイが443で見せる証明書のSHA-256フィンガープリントが/root/istlab-ingress/tls/mall.crtと同じならyes)、gwapi_namespace=、weights=(v1/v2の重み、例50/50)、admin_via_istio_gateway=、admin_via_gateway_api=です。
参考
- このVMは準備に2–4分かかります。
kubectl・istioctl(1.31.0)は、ログインシェルからそのまま使えます。 - ゲートウェイはLoadBalancerのアドレスではなくClusterIPで呼んでください。
kubectl -n istio-system get svc istio-ingressgateway -o jsonpath='{.spec.clusterIP}'です。VMのシェルは、クラスターのServiceアドレスに届きます。 kubectl get gatewayは、IstioとGateway APIのどちらを見るのかが曖昧です。gateways.networking.istio.io・gateways.gateway.networking.k8s.ioのように、グループまで書いてください。- 設定を変えた直後は、ゲートウェイに行き渡るまで数秒かかります。1回で判断せず、何回か繰り返してください。
- よくある失敗: TLS Secretを
mallネームスペースに作るケースです。ゲートウェイのPodがあるistio-systemに置く必要があります。
メッシュにアプリを2系統載せ、ゲートキーパーを探す
kubectl apply -f /opt/fixtures/istlab/ingress-app.yamlでネームスペースmallのweb-v1・web-v2を載せ、2つのDeploymentの準備ができるまで待ってください。その後、/root/istlab-ingress/01-gateway.txtに、Ingressゲートウェイのingress_cluster_ip=(Serviceistio-system/istio-ingressgatewayのClusterIP)とingress_pod=(そのゲートウェイのPod名)の2行を書いてください。
defaultプロファイルは、istiodと一緒にistio-ingressgatewayをあらかじめ立てておきます。このゲートウェイはサイドカーではなく単独で動くEnvoyで、今は何も設定を受け取っていないので、どんなリクエストにも404を返します。Serviceの種類はLoadBalancerなのでk3sがノードのアドレスを付けてくれますが、ラボでは揺らがないClusterIPで呼びます。Pod名は-l istio=ingressgatewayで選んでください。
Istio GatewayとVirtualServiceで入り口を開く
/root/istlab-ingress/gateway.yamlに次の2つのリソースを書いて適用してください。ネームスペースmallに、IstioのGatewayであるmall-gw(セレクターistio: ingressgateway、ポート80のHTTP、ホストmall.example.com)と、VirtualServiceのmall-ingress(ホストmall.example.com、ゲートウェイmall-gw、宛先web-v1.mall.svc.cluster.localのポート80)です。適用後に、Host: mall.example.comでゲートウェイのClusterIPを呼ぶと、v1が返る必要があります。
IstioのGatewayは、すでに起動しているゲートウェイのPodをセレクターで選んで、「このポート・ホストで受け取れ」と伝えるだけで、どこへ送るかは知りません。それは、gatewaysにそのGatewayの名前を書いたVirtualServiceが決めます。どちらか一方だけだと404です。反映に数秒かかるので、レスポンスが変わるまで繰り返してみてください。Kubernetesにはgatewayという名前のリソースが2つ(IstioとGateway API)あるので、取得するときはkubectl get gateways.networking.istio.ioのようにグループまで書くほうが安全です。
ゲートウェイでTLSを終端する
opensslでCN=mall.example.com、SANDNS:mall.example.comの自己署名証明書と鍵を、/root/istlab-ingress/tls/mall.crt・/root/istlab-ingress/tls/mall.keyとして作り、istio-systemにTLS Secretのmall-credとして入れてください。その後、/root/istlab-ingress/gateway.yamlのGatewayに、ポート443のHTTPSサーバー(ホストmall.example.com、tls.mode: SIMPLE、credentialName: mall-cred)を追加して、もう一度適用します。curl --cacert /root/istlab-ingress/tls/mall.crt --resolve mall.example.com:443:<ClusterIP> https://mall.example.com/がv1を返す必要があります。
ゲートウェイは証明書ファイルをマウントしません。credentialNameが指すSecretを、istiodがSDSでゲートウェイのEnvoyに送り込み、Secretを変えれば、ゲートウェイを再起動しなくても新しい証明書を使います。そのためSecretは、ゲートウェイのPodと同じネームスペース(istio-system)に置きます。curl --resolveは、DNSなしで名前をアドレスに結び付け、SNIと証明書検証を一緒に確認させてくれます。ゲートウェイが何を受け取ったかは、istioctl proxy-config secret -n istio-system deploy/istio-ingressgatewayで見ます。
Gateway APIはゲートウェイを新しく立てる
/root/istlab-ingress/gwapi.yamlに次の2つのリソースを書いて適用してください。ネームスペースmallのConfigMapのmall-api-options(data.serviceにspec: {type: ClusterIP})と、Gateway APIのGatewayであるmall-api(gatewayClassName: istio、infrastructure.parametersRefでそのConfigMap、リスナーhttpはポート80のHTTP、ホスト名api.mall.example.com、同じネームスペースのルートだけを許可)です。GatewayがProgrammedになるまで待ってください。
IstioのGatewayと違って、Gateway APIのGatewayはゲートウェイのDeploymentとServiceを自分で作ります(名前は<Gateway 이름>-istioです。プレースホルダーはGatewayの名前です)。デフォルトのServiceの種類はLoadBalancerですが、このVMのk3sは80番ポートをすでにIngressゲートウェイに渡しているため、新しいLoadBalancerがアドレスを受け取れず<pending>のまま留まります。そうするとGatewayがProgrammedになりません(実測)。公式ドキュメントの方法どおり、infrastructure.parametersRefでConfigMapを指して、Serviceの種類を変えてください。状態はkubectl -n mall get gateways.gateway.networking.k8s.io mall-api -o yamlのconditionsにあります。
HTTPRouteでヘッダーと重みを振り分ける
/root/istlab-ingress/httproute.yamlにHTTPRouteのapi-splitを書いて適用してください。親はGatewayのmall-api、ホスト名はapi.mall.example.com、ルールは2つです。ヘッダーx-canary: yesならweb-v2(ポート80)へ、それ以外はweb-v1の80とweb-v2の20の重みで振り分けます。mall-api-istioのServiceのClusterIPにx-canary: yesのリクエストを送ると、常にv2が返る必要があります。
HTTPRouteは、parentRefsでどのGatewayに付くかを自分で示します(IstioのVirtualServiceがgatewaysで示すのと同じ向きです)。ルールは、より具体的なもの(ヘッダー条件)が優先されます。重みは、数回のリクエストでは確認がぶれるので、ゲートウェイのEnvoyが実際に受け取ったルートテーブル、つまりistioctl proxy-config routes deploy/mall-api-istio -n mall -o jsonのweightedClustersで確認してください。
2つのゲートウェイはどこに立っているか
/root/istlab-ingress/06-compare.txtに4行を書いてください。Istio Gatewayが選んだゲートウェイPodのネームスペースistio_gateway_ns=、Gateway APIが作ったゲートウェイPodのネームスペースgwapi_gateway_ns=、そのDeploymentの名前gwapi_deployment=、そのServiceの種類gwapi_service_type=です。
Istio Gatewayは、すでにあるゲートウェイを選んで使い、Gateway APIは、Gatewayがあるネームスペースにゲートウェイを立てます。そのため、チームごとに自分のネームスペースにゲートウェイを置いて、別々に増減できます。その代わり、ゲートウェイが増えた分だけリソースも増えます。作られたものには、ラベルgateway.networking.k8s.io/gateway-name=mall-apiが付きます。
入り口の手前で/adminを塞ぐ、そして塞がれない入り口
/root/istlab-ingress/deny-admin.yamlに、istio-systemのAuthorizationPolicyであるdeny-adminを書いて適用してください。セレクターはistio: ingressgateway、action: DENY、パスは/adminと/admin/*です。適用後、/root/istlab-ingress/07-scope.txtに、istio_gateway_admin=(mall.example.com/adminのステータスコード)とgwapi_admin=(api.mall.example.com/adminをGateway APIのゲートウェイに送ったステータスコード)の2行を書いてください。
ゲートウェイもEnvoyのワークロードなので、認可ポリシーをセレクターで付けられ、ゲートウェイで防げば、リクエストはメッシュの中に入ってきません。ところが、セレクターが選ぶのはistio-ingressgatewayのPodだけです。Gateway APIが立てたゲートウェイは別のPodなので、このポリシーはかかりません。2つ目の入り口を開けた瞬間に、1つ目の入り口の警備がその入り口を守らなくなる、という意味です。その事実を数字で残してください。
入り口が2つのとき何を確認するかを書く
/root/istlab-ingress/08-report.mdに5行を書き、その下に学んだことを4行以上書いてください。5行は、tls_fingerprint_match=(ゲートウェイが443で見せる証明書のSHA-256フィンガープリントが/root/istlab-ingress/tls/mall.crtと同じならyes)、gwapi_namespace=、weights=(v1/v2の重み、例50/50)、admin_via_istio_gateway=、admin_via_gateway_api=です。
フィンガープリントは、openssl s_client -connect <ClusterIP>:443 -servername mall.example.comの証明書とファイルを、openssl x509 -noout -fingerprint -sha256で並べて見れば足ります。残りはステップ6・7の記録から写してください。