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

CCA — Cilium認定アソシエイト

IPが変わってもポリシーが揺れない理由

TT Labで続きを見る

一言でいうと

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が本当にないことを検証するスクリプトを作ります。