IPが変わってもポリシーが揺れない理由
一言でいうと
Ciliumは、Podのラベルの集合に数字(アイデンティティ)を1つ付け、その数字でポリシーを判定します。Podが再スケジュールされてIPが変わっても、ラベルが同じならアイデンティティも同じなので、ポリシーマップには手を付けず、ipcacheの1か所だけを更新すれば済みます。
なぜ必要なのか
IPベースのポリシーの根本的な問題は、KubernetesではIPが一時的な値だという点にあります。Podはたびたび終了して再び起動し、そのたびに新しいアドレスを受け取ります。ポリシーをIPで表現すると、Podが1つ再起動するたびにすべてのノードのルールを再計算する必要があり、その間の短い時間にトラフィックが誤って許可されたり、誤って遮断されたりします。
Ciliumはこの問題を1段上で解決します。ポリシーは「app=frontendがapp=backendの8080にアクセスできる」という意味で表現され、その意味がカーネルまで数字として保たれます。iptablesの時代には、この意味がIPルールに翻訳される過程で失われていました。
どう動くのか
Podが起動すると、agentはセキュリティ関連のラベルだけを選び出して、アイデンティティを計算します。ここで、どのラベルが含まれ、どのラベルが除外されるのかが重要です。
포함되는 라벨 (예) 제외되는 라벨 (예)
k8s:app=frontend k8s:pod-template-hash=7d4f9c
k8s:team=payments k8s:controller-revision-hash=...
k8s:io.kubernetes.pod. k8s:pod-template-generation=...
namespace=shop
pod-template-hashの類が除外される理由は明確です。この値はDeploymentを新しくデプロイするたびに変わります。 アイデンティティに含めると、ロールアウト1回ですべてのPodが新しいアイデンティティを受け取ることになり、ポリシーマップをすべて再計算しなければなりません。ラベルベースのモデルの利点が丸ごと失われてしまいます。
アイデンティティが作られると、すべてのノードのcilium_ipcacheマップに「IP → アイデンティティ」のマッピングが伝播されます。受信側エンドポイントのポリシーマップは、(소스 아이덴티티, 포트, 프로토콜, 방향)(プレースホルダーは送信元アイデンティティ、ポート、プロトコル、方向です)をキーにしてO(1)で判定します。Podが移動してもipcacheだけが変わり、ポリシーマップはそのままという点が、この設計の核心的な利点です。
kube-proxyの置き換えは、Serviceの抽象化を2層のマップで実装したものです。cilium_lb4_services_v2でフロントエンド(IP、ポート)を探し、cilium_lb4_backendsで実際のPodのアドレスを取得してDNATします。これに加えて、ソケットロードバランシングという最適化があります。Pod内のアプリケーションがconnect()を呼び出すその瞬間に、カーネルのソケットフックでServiceのIPをバックエンドのIPに置き換えてしまいます。パケットがそもそもServiceのIPに向けて送られないので、パケット単位のNATもconntrackのエントリも必要ありません。
kubeProxyReplacementを有効にするときにk8sServiceHostとk8sServicePortを一緒に指定しなければならない理由も、ここにあります。kube-proxyがなければ、kubernetes ServiceのClusterIP(通常は10.96.0.1)を実際のAPIサーバーのアドレスに変換する主体はCilium自身ですが、ブートストラップの時点ではそのCiliumがまだ起動していません。鶏と卵の問題を避けるために、実際のアドレスを直接伝えるのです。
ルーティングモードの選択基準は単純です。
| 基準 | トンネル(VXLAN/Geneve) | ネイティブルーティング |
|---|---|---|
| ネットワークの要件 | ノード間のUDPポートが開いていればよい | アンダーレイがPodCIDRの経路を知っている必要がある |
| オーバーヘッド | 約50バイト、MTUの縮小 | なし |
| 適した環境 | アンダーレイを制御できない環境 | BGPピアリングが可能なオンプレミス、クラウドのENI |
負荷分散アルゴリズムでは、Maglev一貫性ハッシュを知っておく必要があります。バックエンドが追加されたり抜けたりしたとき、既存のフローの大半が同じバックエンドに引き続きマッピングされるように、ハッシュテーブルの再配置を最小限に抑えます。複数のノードがECMPで同じVIPを受け取る構成で、ノードごとに同じ選択をさせる効果も大きいです。
現場での姿
筆者のホームラボは、PodCIDR 10.244.0.0/16の上にCilium 1.20.1を載せており、データパスの状態は次のように報告されます。
KubeProxyReplacement: True [enp2s0 10.0.0.117 (Direct Routing)]
Routing: Network: Tunnel [vxlan] Host: BPF
Masquerading: BPF [enp2s0] 10.244.2.0/24
Encryption: Wireguard [cilium_wg0 (Port: 51871, Peers: 2)]
Modules Health: Stopped(0) Degraded(0) OK(92)
読み方があります。Routing: Network Tunnel[vxlan]は、ノード間ではカプセル化を使うという意味で、Host: BPFは、ホストのルーティングまでeBPFが担当しているという意味です。Masquerading: BPFは、SNATもiptablesではなくeBPFで処理されるという意味です。つまり、このノードにはkube-proxyも、マスカレード用のiptablesルールも必要ありません。実際に、KUBE-チェーンは0個でした。
k8sServiceHostには、コントロールプレーンノードの実アドレス10.0.0.120が入りました。このアドレスがどのように決まったのかも教訓です。もともとこのクラスターは、DHCPのリースで.111を使っていましたが、ある日.120を受け取って落ちました。apiserverの証明書のSANに.120が入っていなかったため、IPを元に戻す以外に復旧が難しく、結局staticで固定したうえで、最初から構築し直しました。ブートストラップのアドレスは、あとから変更するのが非常に苦痛な、数少ない設定の1つであることを覚えておくとよいでしょう。
次のラボですること
Cilium Helm valuesファイルを自分で作成してkube-proxyの置き換えとデータパスのオプションを宣言し、ノードにラベルを付けてワークロードの配置を固定し、ServiceとDeploymentを実際にapplyしてバックエンドが認識されるかを確認したあと、kube-proxyが本当にないことを検証するスクリプトを作ります。