K8s 高級 #1 CNI 深さ — Calico / Cilium / eBPF

読了 15分

K8s 高級シリーズの最初の記事です。中級 #7 で NetworkPolicy を扱うときに 1 行残しておきました。「NetworkPolicy は K8s マニフェスト次元では標準だが、実際にトラフィックを止めるのは CNI プラグインがする。」 その 1 行が今回のテーマです。同じ kind: NetworkPolicy マニフェストが Calico の上では iptables ルールに解かれ、Cilium の上では eBPF プログラムに解かれます。形が同じでも実行段の動作・性能・観測可能性は違います。この記事では K8s ネットワークモデルの要求CNI インターフェースの正体データプレーンの 3 モデル(iptables / IPVS / eBPF)Calico と Cilium の比較CNI 選択の実戦基準 までを 1 サイクルでまとめます。

このシリーズは K8s 高級 6 編です。

ヒント
本シリーズの実習記事は YAML マニフェストを手で書いていきます。インデント 1 つ、引用符 1 つがずれただけで kubectl apply が意図と異なるエラーを返し、原因をクラスタ側から逆に辿ることになります。マニフェストを適用する前に utilrepo の YAML 検証ツール に貼り付けておくと、構文エラーを行・列番号で示してくれます。utilrepo はブラウザで動作する軽量な Web ユーティリティ集で、秘密情報が外部に出ず --- で連結された複数文書マニフェストやタブ・スペース混在のような頻出する罠もまとめて拾ってくれます。

K8s ネットワークモデルが要求する 4 つ #

CNI を話す前に、K8s がネットワーク実装に要求する条件から押さえておきます。K8s 自体はネットワーク実装を持っていません。代わりにネットワークが 必ず満たさなければならない 4 つの条件 を仕様として定めており、この条件を満たすことは CNI プラグインの仕事です。

条件説明
Pod-to-PodNAT なしですべての Pod がすべての Pod と通信可能
Node-to-Podすべてのノードのエージェントが NAT なしですべての Pod と通信可能
Pod self IPPod が自分自身を認識する IP と他の Pod がその Pod を呼ぶ IP が同じ
Service 抽象化仮想 IP(ClusterIP)が複数の Pod へ負荷分散

3 つ目の条件はコンテナ環境では自明ではありません。Docker のデフォルトブリッジネットワークは NAT を経由して外部と通信するので、コンテナ内部の自分の IP と外部から見る自分の IP が違います。K8s はこの NAT モデルを拒否します。すべての Pod はクラスタ内で唯一の IP を持ち、その IP で自分自身と他の Pod を全部呼ぶ。 この単純なモデルの上で Service / DNS / NetworkPolicy のような上位オブジェクトが一貫して動きます。

この 4 つの条件を満たす方式は 1 つではありません。ノード間をつなぐオーバーレイを作ることもでき(VXLAN、Geneve)、ルーティングテーブルに直接 BGP で経路を広告することもでき、eBPF でカーネルのパケット処理経路自体を変えることもできます。どの道を選んだかでデータプレーンの性能特性と観測可能性が分かれます。CNI 選択はその道を選ぶ決定です。

CNI — Container Network Interface #

CNI は K8s だけの規格ではなく コンテナランタイムがコンテナにネットワークを付けるときに呼び出す標準インターフェース です。CNCF が管理する仕様で、kubelet 以外にも podman / cri-o / containerd が同じインターフェースを使います。

規格自体はとても単純です。コンテナランタイムが新しいコンテナを作るときに CNI プラグインの実行ファイルを呼び出しながら、コンテナ ID とネットワーク namespace パスを渡します。プラグインはその namespace 内にインターフェースを作り、IP を割り当て、ルーティングテーブルを埋め、結果を JSON で返します。

kubelet → CNI プラグイン呼び出しの意味
1. kubelet が Pod 生成を決定 (Pod に新しい sandbox コンテナを作る)
2. コンテナランタイム(containerd など)が network namespace を生成
3. その namespace パスを引数に CNI プラグイン実行 (ADD コマンド)
4. プラグインが namespace 内に veth インターフェースを作って IP を割り当て
5. プラグインがホスト側のルーティング・iptables・eBPF map などを更新
6. プラグインが割り当てた IP を JSON で返す
7. kubelet がその IP を Pod status に記録

ここで核心は K8s 自体は 4 段階から 6 段階の実際のネットワーク実装を知らない 点です。veth を作ろうが、MACVLAN を作ろうが、eBPF フックでパケットを横取りしようが — その責任は完全に CNI プラグインにあります。K8s は「Pod に IP が付き、上の 4 条件が満たされた」という結果だけ受け取ります。

この分離構造のおかげで同じ K8s クラスタで CNI を入れ替えられます(クラスタセットアップ時点で)。Calico を入れようが Cilium を入れようが Flannel を入れようが、K8s API ユーザーは同じマニフェストを書きます。ただしそのマニフェストが 実際にどう解かれるか が変わります。今回のテーマがその「どう解かれるか」です。

CNI プラグインの 2 つの部分 #

運用クラスタの CNI プラグインは通常 2 つの部分に分かれます。

  • ノードエージェント(DaemonSet) — 各ノードに 1 つずつ立ってルーティング / ポリシー / IP 割り当てを管理します。Calico の calico-node、Cilium の cilium-agent がこの役割を担います。
  • CNI バイナリ/opt/cni/bin/ にインストールされ、コンテナランタイムが直接呼び出します。ノードエージェントが起動時にこのバイナリをノードのディレクトリに展開しておきます。

この 2 つの部分が一緒に動作しながら K8s が要求する 4 条件をノード単位で実装します。マニフェストは単純なのに運用段の形は 2 層に分かれているという点を覚えておくとデバッグが楽になります。Pod に IP が付かない問題は CNI バイナリ・ノードエージェント・kubelet のうちどこで止まっているかを 1 層ずつ追っていく作業です。

データプレーンの 3 モデル #

K8s ネットワークトラフィックが実際に流れる道をデータプレーンと呼びます。クラスタでよく出会うモデルは 3 つです — iptables ベースIPVS ベースeBPF ベース。1 つずつ押さえます。

iptables ベース — もっとも古い道 #

K8s の基本コンポーネントである kube-proxy は最初から iptables をベースに作られました。ClusterIP に入ってくるトラフィックを後ろの Pod IP たちに分散することを iptables の NAT ルールでほぐします。Service 1 つあたりルールが追加され、Pod の IP が変わるたびにルールが更新されます。

このモデルの長所は単純さと互換性です。ほとんどの Linux カーネルが iptables をサポートし、デバッグツール(iptables -Liptables-save)が豊富です。短所は 規模が大きくなると性能が落ちる 点です。iptables はルールを線形に検査します。Service が 1,000 個で各 Service の後ろに 10 個の Pod があれば、1 つのトラフィック決定に平均 5,000 行近くのルールを舐める必要があります。クラスタ規模が中大型に行くとパケットあたり CPU コストが目に見えて上がります。

kube-proxy の iptables ルール一部を見る
sudo iptables -t nat -L KUBE-SERVICES -n
出力例 (Service 1 つあたり 1 行ずつ)
KUBE-SVC-XYZAB1234567  tcp  --  *  *  0.0.0.0/0  10.96.0.10  /* kube-system/kube-dns:dns */
KUBE-SVC-ABCDE9876543  tcp  --  *  *  0.0.0.0/0  10.96.45.12  /* default/web:http */
...

NetworkPolicy が敷かれると iptables ルールがさらに増えます。Calico のデフォルトデータプレーンモードがこの道で、Pod 1 つあたり ingress・egress ポリシールールがホストの iptables チェーンに追加されます。ポリシーの数と Pod の数が掛け算されてルール個数が早く膨らみます。

IPVS ベース — カーネル段の負荷分散 #

IPVS は Linux カーネルが持つ 4 層負荷分散モジュールです。iptables が NAT ルールを一般化したツールなら、IPVS は負荷分散そのもののための専用ツールです。ハッシュテーブルベースで動作するのでルール数が増えても検索コストがほぼ一定です。

K8s の kube-proxy は 1.11 から IPVS モードを正式サポートします。--proxy-mode=ipvs で起動すると ClusterIP 負荷分散が IPVS で行われます。大型クラスタ(Service 数千個以上)では iptables モードより平均 latency と CPU 使用量が一貫して低いです。ただし NetworkPolicy のようなポリシー次元は依然として iptables(または nftables)が担当するので、IPVS は部分的な改善です。

eBPF ベース — カーネル自体を書き換える道 #

eBPF(extended Berkeley Packet Filter)は Linux カーネル内にユーザー定義プログラムを安全に挿入できるメカニズムです。元はパケットフィルタリング用に始まりましたが、今ではカーネルのほぼすべてのフックポイント(システムコール、ネットワーク処理段階、トレーシングポイント)に小さなプログラムを掛けられます。

ネットワーク面で eBPF が意味のある理由は単純です。iptables / IPVS の役割を eBPF プログラムが代替でき、同じ結果をより少ない CPU でより豊かな観測情報と一緒に作り出せます。 パケットがカーネルを通過する道に直接コードが挿入できるので、NAT・負荷分散・ポリシー検査が 1 セットで処理されます。iptables ルールを線形検査することもなく、NetworkPolicy のために別途チェーンを作ることもありません。

iptables モデル vs eBPF モデル — 同じトラフィック 1 件
[iptables モデル]
  パケット → conntrack → KUBE-SERVICES チェーン → KUBE-SVC-XXX → KUBE-SEP-YYY → DNAT → ルーティング
        (ルール N 個の線形検査)

[eBPF モデル]
  パケット → tc/XDP フックの eBPF プログラム
        → eBPF map 照会(Service → Pod リスト、O(1))
        → ポリシー map 照会(許可するか、O(1))
        → DNAT 後に転送

Cilium がこのモデルの代表実装です。Calico も 3.13 から eBPF データプレーンモードをオプションで提供します。2 製品の違いは次の節で押さえます。

Calico と Cilium — 2 つの道 #

K8s クラスタでもっともよく出会う CNI プラグインが Calico と Cilium です。両方とも NetworkPolicy を完全にサポートし、両方とも運用規模で動いた実績が十分積まれています。違いは データプレーンのデフォルトモデルeBPF にどれだけ深く依存するか にあります。

Calico — BGP・iptables がデフォルト、eBPF はオプション #

Calico のデフォルトデータプレーンは 2 つの部分の結合です。

  • ノード間ルーティング — BGP(Border Gateway Protocol)で各ノードの Pod CIDR を他のノードに広告します。ノード自体がルーターのように動作するのでオーバーレイ(VXLAN のようなカプセル化)が必要ありません。クラウドのルーティングテーブルが Pod CIDR を知らない環境では IP-in-IP や VXLAN でカプセル化するオプションもあります。
  • ノード内ポリシー / NAT — iptables で Service 負荷分散と NetworkPolicy を解きます。kube-proxy の iptables モードと同じ位置です。

この組み合わせの長所は運用段の馴染みです。ルーティングが BGP で動くのでデータセンターの BGP インフラ(特にオンプレミス、ToR スイッチ環境)と自然に組み合わさります。iptables ルールは標準ツールでデバッグできます。短所は 規模が大きくなると iptables の限界がそのまま来ます。 Service / Pod / ポリシーの数が増えるとルール個数が早く膨らみ、kube-proxy の同期時間も長くなります。

Calico 3.13 からはデータプレーンを eBPF に入れ替えるモードが追加されました。このモードでは kube-proxy がもう必要なく、Service 負荷分散と NetworkPolicy がすべて eBPF で解かれます。ただし Calico の BGP ルーティングモデルはそのまま維持されるので、「ルーティングは BGP、データプレーンは eBPF」のハイブリッドな形になります。

Cilium — 最初から eBPF #

Cilium は最初から eBPF を前提に設計された CNI です。Service 負荷分散、NetworkPolicy、7 層ポリシー(HTTP・gRPC のメソッド単位の許可/拒否)、ノード間暗号化(WireGuard / IPsec)、観測可能性(Hubble)まですべて eBPF プログラムで解きます。

Cilium のコンポーネント構成 (単純化)
[各ノード]
  cilium-agent (DaemonSet)
    ├─ eBPF プログラムをコンパイル・ロード
    ├─ Service / Endpoint / NetworkPolicy を eBPF map に埋める
    └─ Pod 生成時に veth + eBPF フック付加

  Hubble (オプション)
    └─ eBPF から収集したフロー・メトリクスを公開

Cilium の差別点は 3 つです。

  • kube-proxy 代替 — Cilium だけで Service 負荷分散が処理されるので kube-proxy を切れます。クラスタのコンポーネント数が減り、iptables ルールが消えるのでノードのパケット処理経路が短くなります。
  • 7 層ポリシー — NetworkPolicy の標準スペックは 4 層(IP / ポート)に限定されますが、Cilium は独自 CRD(CiliumNetworkPolicy)で HTTP メソッド・パス、gRPC サービス / メソッド、Kafka トピック単位のポリシーを表現できます。このポリシーも eBPF プログラムで解かれます。
  • Hubble — eBPF ベースの観測可能性 — パケットが eBPF フックを通るときにメタデータを収集してフロー単位の可視性を提供します。「どの Pod がどの Pod のどのポートを呼ぶか」をリアルタイムで見られます。観測可能性は #5 で別途扱いますが、Hubble が eBPF の副産物として自然に付いてくるという点は Cilium の魅力の 1 つです。

一目で見る比較 #

次元Calico (デフォルト)Calico (eBPF モード)Cilium
Pod ルーティングBGP / IP-in-IP / VXLANBGP / IP-in-IP / VXLANVXLAN / Geneve / native routing
Service 負荷分散kube-proxy (iptables/IPVS)eBPFeBPF (kube-proxy 代替可能)
NetworkPolicy 実行iptableseBPFeBPF
7 層ポリシー非対応(標準スペック)非対応CiliumNetworkPolicy で対応
観測可能性外部ツール必要外部ツール必要Hubble 内蔵
運用ツール馴染み標準 iptables ツールeBPF デバッグ必要eBPF デバッグ必要
最初の導入の参入障壁

同じ K8s マニフェストが 3 列のどこでも動作します。ただし そのマニフェストがノード内でどんな形に解かれるか は列ごとに違います。この違いが性能・観測可能性・運用ツールの選択にそのまま反映されます。

eBPF が変えること #

CNI 選択を話すときに eBPF がよく登場するので、eBPF が K8s ネットワークにもたらした変化を一度押さえておきます。eBPF 自体は K8s コンポーネントではなく Linux カーネル機能ですが、K8s ネットワークのデータプレーンがこの機能を積極的に使いながら運用モデルの肌理が変わりました。

kube-proxy の役割が消える #

長い間 kube-proxy は K8s クラスタの必須コンポーネントでした。ClusterIP の仮想 IP を実際の Pod IP たちに分散することがこのコンポーネントの責任で、その仕事を iptables または IPVS で解いていました。

Cilium が kubeProxyReplacement: true オプションで kube-proxy を完全に代替でき、Calico の eBPF モードも同じことをします。コンポーネント 1 つが抜けると運用の表面積がそれだけ減ります — モニタリング対象が 1 つ減り、同期遅延を疑う候補が 1 つ減り、ルール急増の原因が 1 つ消えます。

NetworkPolicy のコストモデルが変わる #

iptables ベースの NetworkPolicy はポリシーの数と Pod の数に比例してルールが増えます。eBPF ベースではポリシーが map で表現されるので検索コストがほぼ一定です。ポリシー数が数百・数千に増えるマルチテナントクラスタでこの違いがパケットあたり latency に現れます。

観測可能性がデータプレーンの副産物になる #

伝統的モデルでトラフィック可視性は別作業でした — Pod にサイドカーを付けるか、NodePort に tcpdump を掛けるか、別途モニタリングエージェントを敷くか。eBPF データプレーンではパケットがどうせ eBPF フックを通るので、その道でメタデータ(ソース Pod / 宛先 Pod / ポリシー検査結果 / latency)を一緒に収集すれば自然にフロー単位の可視性が作られます。Cilium の Hubble がこのモデルの直接的な結果物です。

CNI 選択 — 実戦基準 #

理論的にはどの CNI も K8s が要求する 4 条件を満たします。しかし運用クラスタの CNI 決定は次の 5 つの次元が集まる決定です。

次元質問
クラスタ規模Service / Pod / NetworkPolicy の数が何桁か
ネットワーク環境クラウドマネージドか、オンプレミス BGP インフラか
7 層ポリシー必要可否HTTP / gRPC 単位ポリシーが運用要求に入っているか
運用チームの馴染み度iptables デバッグに慣れているか、eBPF ツールに行く意向があるか
マネージド K8s のデフォルトEKS / GKE / AKS のデフォルト CNI をそのまま使うか、入れ替えるか

マネージド K8s にはクラウド事業者が推すデフォルト CNI があります。EKS は aws-vpc-cni(VPC IP を Pod に直接割り当てるモデル)、GKE は独自 CNI(VPC-native モード)、AKS は Azure CNI または kubenet です。このデフォルト CNI はそのクラウドのネットワーキングともっとも滑らかに組み合わさりますが、NetworkPolicy サポートや eBPF 機能が不足することがあって、運用要求に応じて Calico / Cilium に入れ替えるケースが多いです。EKS では aws-vpc-cni を維持しながら NetworkPolicy だけ Calico または Cilium の chained モードで載せるパターンがよくあります。

小規模・単純クラスタではマネージド事業者のデフォルト CNI をそのまま使う決定がもっとも合理的です。運用負担がもっとも小さく、クラウドサポートとの呼吸ももっとも良いです。NetworkPolicy 要求が本格的に入ってくる段階、またはマルチテナント隔離・7 層ポリシー・きめ細かなフロー可視性が運用要求になる段階で Calico / Cilium の導入検討が自然に始まります。

選択の細かい肌理を 1 行ずつ整理します。

  • Calico (デフォルトモード) — BGP インフラが既にあり、iptables デバッグに慣れているチームがもっとも早く導入できる道です。中小規模クラスタでもっとも負担が少ない選択です。
  • Calico (eBPF モード) — ルーティングはそのまま置いてデータプレーン性能だけ引き上げたいときに使うモードです。BGP 資産を生かしながら eBPF の利点を持ってくる折衷です。
  • Cilium — 7 層ポリシー・Hubble 観測可能性・kube-proxy 削除を 1 セットで持っていきたいときに合う選択です。eBPF に本格的にベットする選択です。

この決定は一度下すと変えにくいです。CNI の交換はクラスタ全体のネットワーク再設定に近く、通常はクラスタ新規セットアップ時点で決まります。そのため最初に決定する際には、運用の今後 1〜2 年の見通しを合わせて考えておく方が良いです。

締めくくり #

K8s 高級シリーズの最初の記事を締めくくります。この記事では K8s ネットワークモデルが要求する 4 条件から始め、その条件を満たす責任が CNI プラグインにある点、そのプラグインがデータプレーンを iptables / IPVS / eBPF のどの道で解くかによって同じマニフェストが違う形で動く点を追いました。Calico の BGP + iptables モデルと Cilium の最初から eBPF モデルを比較し、eBPF が kube-proxy の役割を代替して NetworkPolicy のコストモデルを変えて観測可能性をデータプレーンの副産物にした流れまで押さえました。次の記事では同じ流れで 中級 #7 RBAC で 1 行に留めた部分を深掘りします — Aggregated ClusterRole、Impersonation、EKS の IRSA と GKE の Workload Identity のように、K8s 権限モデルが外部 IAM と組み合わさる道を扱います。

X