プロキシをPodから外すと何が変わるのか
一言でいうと
アンビエントモードは、L4とL7を分離して、すべてのワークロードがフルのEnvoyのコストを強制的に負担しなくて済むようにした構造です。その代わり、障害ドメインがPodからノードへ大きくなります。
なぜ必要なのか
サイドカーモデルのコストは、数字で見ると明らかです。Pod 500個のクラスターで、サイドカー1つあたりのメモリが120MiBなら、それだけで約58GiB、CPU requestが100mずつなら50 vCPUが、スケジューリングの容量から消えます。しかも、プロキシがPodの中にあるので、バージョンを上げるには全ワークロードをローリング再起動する必要があり、注入ラベル・リビジョンタグ・Webhookの設定のうち1つでも食い違えば、「あるPodはメッシュの中、あるPodはメッシュの外」という状態になります。この状態でSTRICTを有効にすると、注入されていないPodがすべて切断されます。
ここに、決定的な観察がもう1つ加わります。実際にL7の機能が必要なサービスは、全体の一部です。残りの大多数は、mTLSと基本的なテレメトリーで十分なのに、サイドカーモデルは、これらにもフルのEnvoyを負担させていました。
どう動くのか
アンビエントは、2つの層に分かれます。
- ztunnel: ノードごとに1つずつ起動するDaemonSetです。Rustで書かれた軽量プロキシで、Envoyではありません。役割は意図的にL4に限定されます。mTLS、SPIFFEのアイデンティティに基づくL4の認可、TCPのテレメトリーまでです。HTTPをパースしないので、メモリはPodの数ではなく、接続の数に比例します。
- waypoint: L7が必要なネームスペースやServiceAccount単位でだけデプロイする、Envoyベースのプロキシです。Gateway APIのGatewayリソースとして宣言し、通常のDeploymentなので、HPAで別にスケールします。
ノード間は、HBONEトンネルで流れます。ポート15008で、mTLSの上にHTTP/2 CONNECTで、元のTCPストリームを包む方式です。HTTP/2のストリーム多重化のおかげで、同じノードの組の複数の接続が1つのmTLS接続を共有してハンドシェイクのコストが減り、CONNECTヘッダーに元の宛先とアイデンティティが載るので、受信側のztunnelは、ペイロードを開けなくてもL4の認可を判定できます。
設計の原則を1つ、必ず覚えておく必要があります。waypointは宛先側の所有物です。サイドカーモデルでは、クライアント側のプロキシがルーティングとリトライを行いましたが、アンビエントでは、宛先のサービスを所有するチームが、自分のwaypointでL7のポリシーを執行します。ポリシーの所有権が明確になる代わりに、「呼び出し元ごとに異なるタイムアウト」のようなクライアント側のポリシーは、再設計が必要です。
正直にトレードオフも見る必要があります。waypointを経由する経路は、ホップが3つ(ztunnel → waypoint → ztunnel)なので、サイドカー(ホップ2)より長くなることがあります。そして、ztunnelが落ちると、そのノードのすべてのメッシュトラフィックが影響を受けます。サイドカーは、プロキシの障害がPod 1つに閉じ込められていましたが、アンビエントはノード単位の障害ドメインです。PodDisruptionBudget、優先度クラス、再起動のモニタリングを、運用設計に必ず反映する必要があります。
デバッグは、順序がすべてです。istioctl proxy-configを、トラフィックが流れる順序のとおりに読みます。
listener 이 프록시가 그 포트를 듣고 있는가
->
route VirtualService 가 라우팅 테이블로 번역됐는가
->
cluster DestinationRule(서킷브레이커, TLS)이 반영됐는가
->
endpoint 그 subset 에 실제 파드 IP 가 잡혀 있는가
どの段階で期待と食い違っているかを見つければ、直すべきリソースが自動的に決まります。503のレスポンスフラグの判読表も、同じ文脈です。
| フラグ | 意味 | 最初に疑うこと |
|---|---|---|
| UH | no healthy upstream | subsetのラベルとPodのラベルの不一致 |
| UO | upstream overflow | connectionPoolの上限超過 |
| UF | upstream connection failure | 片側だけSTRICTのmTLSの不一致、ポートのプロトコルの誤認 |
| NR | no route | VirtualServiceのマッチの漏れ、catch-allの不在 |
| URX | max retries reached | リトライの使い切り。根本原因はほかのフラグと一緒に追跡します |
オブザーバビリティの側で、最後に1つ。Envoyはスパンを自動で作りますが、アプリケーションがインバウンドのリクエストのトレースヘッダーをアウトバウンドに伝播しないと、トレースが途切れます。メッシュを入れたのにトレースがばらばらに出る理由は、ほとんどがこれであり、メッシュが代わりに行えない唯一の部分でもあります。
現場での姿
筆者のホームラボで、hubble-relayとhubble-uiがPendingのままだったことがあります。イベントは0/1 nodes are available: 1 node(s) had untolerated taint(s)でした。原因は単純でした。コントロールプレーンのノードには、node-role.kubernetes.io/control-plane:NoScheduleのtaintがかかっていて、この2つの構成要素は、DaemonSetではなくDeploymentなので、tolerationがありませんでした。CoreDNSはデフォルトのtolerationを持っていて正常に起動したのと対照的で、ワーカーがジョインするとすぐに解消しました。
この事例が、アンビエントを理解するのにそのまま使えます。ztunnelはDaemonSet、waypointはDeploymentです。つまり、ztunnelはノードごとに自動で起動しますが、waypointは、スケジューラーが置き場所を見つけてやる必要があります。taintがかかったノードだけが残っていたり、リソースが不足していたりすると、waypointは静かにPendingのままになり、そのネームスペースのL7のポリシーは、まったく執行されません。「L4は動くのにL7のポリシーだけが効かない」という症状の最初の確認箇所が、waypointのPodの状態である理由です。
もう1つ。そのホームラボは、--skip-phases=addon/kube-proxyでkube-proxyを、インストールして削除したのではなく、最初から未インストールで構築しました。一度でも動くと、KUBE-SERVICES / KUBE-SVC-* / KUBE-SEP-*のチェーンがノードに残るからです。サイドカーからアンビエントへ移行するときにも、同じ原則が当てはまります。1つのネームスペースに、注入ラベルとambientラベルが共存してはならず、注入ラベルを消したあと、必ずロールアウトの再起動で既存のサイドカーを取り除いてから、ambientラベルを付ける必要があります。
次のクイズで確認すること
このモジュールは概念のモジュールです。ラボの代わりに、クイズでアンビエントの構造、HBONE、障害ドメイン、proxy-configを読む順序、レスポンスフラグの判読を点検します。この5つは、試験だけでなく、実際の障害対応で最初に取り出して使うツールです。