503が出ているのに、設定はすべて適用済みだという
目標
本物のIstio 1.31のメッシュ(VM内のk3s)で、引き継いだ設定が引き起こした4つの症状(ヘッダールールの無視、/apiの503、
カナリアヘッダーの503、サイドカーのないクライアントの接続切断)を、証拠から原因へ絞り込んで直します。
最後に、istiodをいったん停止して、コントロールプレーンがないときに何が続き、何がブロックされるかを、自分の目で見ます。
なぜ重要なのか
メッシュでの503は、原因が1つではありません。設定が指すクラスターがそもそもない場合(NC)、クラスターはあるのに選ぶPodが
ない場合(UH)、リクエストがHTTPとして解釈すらされなかった場合(NR)もあります。kubectl applyが成功したというのは、APIサーバーが
受け取ったという意味にすぎず、Envoyがそのルールをどう使うかは、別に確認する必要があります。そのため、問題解決は3つの層に分けます。
- 設定:
istioctl analyzeが、参照が切れた箇所(IST0101)と、Podを選べないsubset(IST0173)を見つけ出します。 - データプレーン:
istioctl proxy-config listeners·endpointsでEnvoyが実際に受け取った設定を見て、アクセスログの レスポンスフラグで、リクエストがどこで止まったかを読みます。 - コントロールプレーン:
istioctl proxy-statusでどのプロキシがistiodにつながっているかを見て、istiodがないとき、すでに受け取った設定は 使われ続けるものの、新しい設定の書き込みと新しいPodの注入はWebhookのために拒否されることを確認します。
ステップ
- 引き継いだ設定を適用して、4つのPodのuidとサイドカーの有無を記録します。
- ヘッダールールが無視される理由をリスナーで見つけ(tcp_proxy)、Serviceのポート名でプロトコルを正します。
- Telemetry APIで、ネームスペースのアクセスログを有効にします。
/apiの503をアクセスログ(NC)とanalyzeで確認して、存在しないsubsetの参照を直します。- カナリアヘッダーの503をアクセスログ(UH)と空のエンドポイントで確認して、subsetのラベルを直します。
- サイドカーのないlegacyの切断をサーバーのログ(NR)で確認して、STRICTを維持したままメッシュに入れます。
- istiodを停止してから起動し直し、既存のトラフィック・設定の書き込み・新しいPodが、それぞれどうなるかを記録します。
- 証拠のファイルから書き写したレポートを書きます。
参考
- このVMは、準備に数分かかります。
kubectl・istioctlは、ログインシェルからそのまま使えます(istioctl 1.31.0)。 - レシピが作成した
ica-policyネームスペースは、別のラボ用なので触れません。作業ファイルは/root/ica-debug/に置きます。 - 設定を直した直後は、プロキシに伝播するまで数秒かかります。結果が予想と違う場合は、少しあとにもう一度確認してください。
- ステップ7でistiodを元に戻さないと、Istioリソースの変更と新しいPodの作成が、拒否され続けます。
- 公式ドキュメント: Debugging Envoy and Istiod・ Protocol Selection・ Envoy Access Logs・ Configuration analysis messages・ Envoy access log response flags
引き継いだメッシュのPodとプロキシの一覧を書く
引き継いだ設定/opt/fixtures/ica-debug/incident.yamlをそのまま適用し、ネームスペースica-debugの4つのPod(web-v1・web-v2・client・legacy)がReadyになるまで待ちます。その時点の一覧を、/root/ica-debug/inventory.jsonにJSON配列として保存します。要素ごとに、name・uid・sidecar(Podにistio-proxyがあればtrue)の3つのキーです。設定はまだ直しません。
何がメッシュに入っているかをまず書いておいて初めて、あとの症状を解釈できます。このVMのIstio 1.31は、istio-proxyをspec.containersではなくspec.initContainersにrestartPolicy: Alwaysで入れます(Kubernetesのネイティブサイドカー)。そのため、2つのリストを両方とも見て初めて、sidecarを正しく判断できます。そして、istioctl proxy-statusで、どのPodがistiodにつながっているかも一緒に見てください。legacyのPodは、ラベルで注入を拒否しています。uidはクラスターが与えた値なので、でっち上げると失敗します。
ヘッダールールを書いたのに、トラフィックがただ半々に流れる
clientからx-canary: yesヘッダーを付けてhttp://web/を何度も呼び出すと、v1とv2が混ざって出てきます(ルールどおりなら、片方だけが出るはずです)。直す前に、clientのサイドカーの80ポートのリスナーを、istioctl proxy-config listeners client.ica-debug --port 80 -o jsonで/root/ica-debug/listener-before.jsonに保存します。そのあと、Service webのポート名をhttp-webに変えて、IstioがこのポートをHTTPとして扱うようにします(ポート番号とtargetPortはそのままです)。
VirtualServiceのhttpルールは、EnvoyがそのポートをHTTPとして解釈するときにだけ使われます。Istioは、Serviceのポートの名前のプレフィックス(<프로토콜>-이름)かappProtocolで、プロトコルを決めます(プレースホルダーはプロトコルと名前です)。保存したリスナーのJSONで、ServiceのClusterIPのリスナーのフィルター名が、tcp_proxyなのかhttp_connection_managerなのかを探してみてください。ポート名は、kubectl patch svcのjson patchで変えられます。
プロキシに何があったかを語らせる
ica-debugネームスペース全体で、Envoyのアクセスログを有効にします。Telemetryリソースを/root/ica-debug/telemetry.yamlに書いて適用してください。spec.accessLoggingのprovider名はenvoyです。有効にしたあとは、clientから送ったリクエストが、kubectl logs client -c istio-proxyに1行ずつ残る必要があります。
minimalプロファイルは、meshConfigにアクセスログのファイルを指定しないので、デフォルトではログが残りません。メッシュ全体の設定を直す代わりに、Telemetry APIでネームスペースの範囲で有効にできます(apiVersion telemetry.istio.io/v1)。リクエストにx-request-idヘッダーを自分で付けておくと、ログでその行を簡単に見つけられます。
/apiだけ503、でもサーバーのPodは問題ない
clientからhttp://web/api/ordersを呼び出すと、503になります。そのリクエストに対応するclientのサイドカーのアクセスログの1行を、そのまま/root/ica-debug/nc.logに保存したあと、原因を直します。VirtualService webのapiルールが指すsubsetを、DestinationRuleに実際にあるv1に変えます。直したあと、/api/ordersは200と本文v1を返す必要があり、istioctl analyze -n ica-debugにIST0101が出ていない必要があります。
Envoyのアクセスログの応答コードのすぐあとの欄が、レスポンスフラグです。フラグとそのあとの詳しい理由を、Envoyのドキュメントのresponse flagsの表と突き合わせてください。サーバーのサイドカーには、このリクエストが届いていないという点も手がかりです。istioctl analyzeは、同じ原因を設定だけを見て見つけ出します。
カナリアヘッダーを付けるとno healthy upstream
clientからx-canary: yesヘッダーでhttp://web/を呼び出すと、今度はno healthy upstreamと503が返ってきます。そのリクエストのclientのサイドカーのアクセスログの1行を、そのまま/root/ica-debug/uh.logに保存し、同じ時点のistioctl proxy-config endpoints client.ica-debug --cluster 'outbound|80|v2|web.ica-debug.svc.cluster.local' -o jsonの出力を、/root/ica-debug/endpoints-before.jsonに保存します。そのあと、DestinationRule webのsubset v2が、実際のPodのラベル(version: v2)を選ぶように直します。
NCと違って、今回はクラスター(outbound|80|v2|…)はあります。問題は、そのクラスターに入ったエンドポイントです。subsetは、Serviceのエンドポイントをさらに、Podのラベルで1段絞り込むものなので、ラベルの値が1文字違うだけで、空のリストになります。kubectl get pod --show-labelsと、DestinationRuleのlabelsを並べて見てください。1.31のanalyzeは、この場合をIST0173として知らせてくれます。
サイドカーのない古いクライアントだけ接続が切れる
legacyのPodからcurl http://web/を実行すると、応答なしで接続が切れます(curlの終了コード56)。サーバー側(web-v1またはweb-v2)のサイドカーのログから、legacyのPodのIPが送信元として記録された行を探して、そのまま/root/ica-debug/nr.logに保存します。そのあと、STRICTはそのままにして、legacyをメッシュに入れて直します。注入を拒否していたラベルを外したPodの定義を/root/ica-debug/legacy.yamlに書いて、Podを作り直します。直したあと、legacyからhttp://web/が200である必要があります。
PeerAuthenticationがSTRICTのサーバーのサイドカーは、mTLSで入ってくる接続だけを受け入れます。サイドカーのないクライアントは平文を送り、サーバーのサイドカーは、その接続に合うフィルターチェーンを見つけられません。HTTPリクエストとして解釈される前の段階なので、ログのメソッド・パスの欄が空です。PERMISSIVEに下げることも、接続はできるようにしますが、このステップの答えではありません。Podのラベルやアノテーションは、実行中に変えても注入されないので、削除して作り直す必要があります。
istiodが停止している間、何が止まるのか
istiodをkubectl -n istio-system scale deploy istiod --replicas=0で停止し、Podが消えたことを確認したあと、3つのことを行って、結果を/root/ica-debug/istiod-down.txtに3行で書きます。traffic=のあとに、clientからhttp://web/を呼び出した本文、config-write=のあとに、kubectl -n ica-debug annotate virtualservice web lab.example/probe=1 --overwriteの出力、new-pod=のあとに、kubectl -n ica-debug run probe --image=curlimages/curl:8.10.1 --restart=Never --command -- sleep 60の出力です。記録したあと、istiodをreplicas 1に戻し、4つのプロキシ(client・web-v1・web-v2・legacy)が、再びistioctl proxy-statusに現れるまで待ちます。
コントロールプレーンは、設定を配り、証明書を発行し、注入・検証のWebhookを受けます。すでに設定を受け取ったEnvoyは、istiodがなくても、最後の設定で動作し続けます。一方、Istioリソースを書き込むリクエストと、新しいPodの作成は、APIサーバーがWebhookを呼ぶ必要があり、WebhookのfailurePolicyが何なのかを、kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -o yamlで確認してみてください。元に戻すのを忘れると、あとのすべての設定変更がブロックされます。
4つの503・切断を、証拠で分類したレポート
/root/ica-debug/report.jsonに、JSONオブジェクトを1つ書きます。キーと値の意味は次のとおりです。header_rules_ignored_filterは、直す前のリスナーで見たネットワークフィルター名の最後の部分(例: xxx_proxy)、api_503_flag・canary_503_flag・legacy_reset_flagは、nc.log・uh.log・nr.logに記録されたレスポンスフラグ、istiod_down_trafficは、istiodがないときに受け取った本文、istiod_down_config_writeは、そのとき設定の書き込みができたならaccepted、拒否されたならrejectedです。値は、自分が残した証拠ファイルと合っている必要があり、4つの症状は、今はすべて直っている必要があります。
レポートは、記憶ではなく証拠から書き写します。ログの行で応答コードの次の欄を読み、リスナーのJSONで、ServiceのClusterIPのリスナーのフィルター名を探してください。採点ツールは、同じファイルを読んで突き合わせ、ヘッダー・/api・legacyのリクエストを今もう一度送って確認します。