Kubernetes 解体新書Part 2 ネットワーク

Pod に NIC と IP を渡すプラグイン

Pod に IP アドレスを付け、通信できる状態にするのは誰でしょうか。ネットワークプラグインの仕事を追います。

  • L2 ノードの中
  • 更新 2026年10月6日

Pod を作ると、その Pod に IP アドレスが付きます。 前の章で手作業したケーブルの接続やアドレスの設定を、起動のたびに誰かが行っています。

このクラスタでその仕事を担当するのは、Cilium というネットワーク用のプログラムです。 この章では、Pod の起動をきっかけに Cilium が呼ばれ、ネットワークを準備する流れを見ます。 その呼び出し方の取り決めを CNI と呼びます。

GOAL この章のゴール

  • Pod の起動時にネットワークが準備される流れを説明できる
  • ネットワークプラグインごとの接続方式の違いが分かる
  • ノードでプラグインの設定と、Pod の経路を確認できる

CNI の 3 つの仕事

Pod に通信できる状態を用意するには、接続口を作り、IP アドレスを設定し、相手まで届く経路を用意します。 これらをネットワークプラグインなどが分担し、コンテナランタイムは CNI の取り決めでプラグインを呼び出します。 CNIシーエヌアイContainer Network Interface。コンテナにネットワークを設定するプラグインの呼び出し方 (環境変数、stdin の JSON、stdout の結果) を定めた仕様と、その仕様に従うプラグイン群。用語集で見る の仕様が決めているのは呼び出し方だけで、Kubernetes が求める仕事は次の 3 つに分かれます。

CNI の 3 つの仕事: ノード間の疎通、Pod への vNIC、IP の割り当て2 台のノードの間をトンネルや BGP、VPC の経路で繋ぎ (1)、各 Pod に veth を差し (2)、ノードごとの Pod CIDR から IP を配る (3)。Node A10.0.1.10podCIDR 10.244.1.0/24web10.244.1.5api10.244.1.6cilium-agent / IPAMNode B10.0.1.11podCIDR 10.244.2.0/24db10.244.2.3cache10.244.2.4cilium-agent / IPAM(1) ノード間の疎通VXLAN / Geneve トンネル、BGP、クラウドの VPC ルート(2) Pod に veth を差す(3) IP を配る (IPAM)PodCIDR はノードごとに分かれている (kube-controller-manager --allocate-node-cidrs)
図 1CNI プラグインの 3 つの仕事。ノード間の疎通を担保し、Pod に veth を差し、ノードごとの PodCIDR から IP を配る。
  1. ノード間の疎通を担保する:Node A の Pod から Node B の Pod へ直接届くように、VXLAN トンネル、BGP、クラウドの VPC ルートなどで経路を作る。
  2. Pod に NIC を差す:コンテナランタイムが作った netns に veth を差す。第 2 章で手でやった作業です。
  3. Pod の IP を配る (IPAM):Pod は頻繁に作られては消えるので、どの IP が使用中かを常駐プログラムが記録し、払い出しと回収をする。IP Address Management の略。

kubeadm init 直後の Node は NotReady で、CNI プラグインを入れると Ready になります。 kubeadm のドキュメントは「Pod 同士が通信するには CNI ベースの Pod network add-on が必要で、入れるまで control-plane node は NotReady のままになる」と書いています。

プラグインの実行モデル

CNI の仕様 (2026 年現在 1.1.0) は、ランタイムがプラグインをどう呼ぶかを次のように定めています。

CNI プラグインの実行モデル: 環境変数 + stdin の JSON で exec、結果は stdout の JSONcontainerd が /etc/cni/net.d の conflist を読み、/opt/cni/bin のバイナリを CNI_COMMAND=ADD などの環境変数付きで exec する。設定は stdin、結果 (interfaces / ips / routes / dns) は stdout。containerd (CRI)RunPodSandbox のときnetns を作ってからプラグインを exec/etc/cni/net.d/05-cilium.conflist"plugins": [ {"type": "cilium-cni"} ]名前順で最初の 1 つを使う読むexec /opt/cni/bin/cilium-cniCNI_COMMAND=ADD CNI_CONTAINERID=…CNI_NETNS=/run/netns/cni-… CNI_IFNAME=eth0CNI_PATH=/opt/cni/bin CNI_ARGS=K8S_POD_NAME=…stdin: conflist の中身 (JSON)execstdout: 結果の JSON"interfaces": [{"name":"eth0","sandbox":"/run/netns/…"}]"ips": [{"address":"10.244.0.23/32","gateway":…}]"routes": […], "dns": {…}exit 0ADD / DEL / CHECK / GC / STATUS / VERSION の 6 操作 (CNI spec 1.1)
図 2CNI プラグインは exec される普通の実行ファイル。設定は conflist と stdin、呼び出し内容は環境変数、結果は stdout の JSON。
  • ランタイムは「プラグインを呼ぶ前に、コンテナ用の network namespace を作っておかなければならない」。
  • 操作は環境変数 CNI_COMMAND で渡す。ADD (ネットワークに追加)、DEL (取り外し)、CHECK (期待どおりか検査)、GC (使われていない資源の掃除)、VERSION に加え、プラグインの準備状態を聞く STATUS がある。
  • そのほかの環境変数は CNI_CONTAINERID (コンテナの ID)、CNI_NETNS (netns のパス。たとえば /run/netns/[nsname])、CNI_IFNAME (コンテナ内に作る NIC の名前)、CNI_ARGS (FOO=BAR;ABC=123 形式の追加引数)、CNI_PATH (プラグインの実行ファイルを探すパス)。
  • 設定は JSON で stdin から渡し、成功したら結果を JSON で stdout に返す (cniVersion、作った interfaces、割り当てた ips、routes、dns)。失敗なら stderr にエラーを出し、終了コードは非 0。
  • 設定ファイル (conflist) の plugins 配列に複数のプラグインを並べると、ランタイムは順に exec し、前のプラグインの結果を prevResult として次に渡す (chaining)。DEL は逆順。

仕様自体にはファイルの置き場所は書かれていません。 Kubernetes のドキュメントは既定の設定ディレクトリを /etc/cni/net.d、実行ファイルの置き場所を /opt/cni/bin としていて、containerd は設定ディレクトリのファイルを名前順に並べ、既定では最初の 1 つだけを読みます。

Pod が 1 つ起動するときの流れを、ステップごとに見ます。

kubeletapiserver を watchCRI gRPCcontainerdCRI plugin/etc/cni/net.d/05-cilium.conflistどのプラグインを呼ぶかの設定 (JSON)/opt/cni/bin/cilium-cniexec (ADD) / 環境変数 + stdinstdout: interfaces / ips / routes結果 JSON → containerd → kubeletcilium-agentノードの DaemonSetIPAMpodCIDR 10.244.0.0/24netns: cni-7f3a… (Pod)eth0 (veth)10.244.0.23/32nginx後から joinlxc7f3a (host 側)tc eBPF が転送を担当
kubelet: Pod default/web-7c9d が nodeName=このノード
kubelet → containerd: RunPodSandbox(web-7c9d) over /run/containerd/containerd.sock

1/7kubelet が自分宛の Pod を見つけ、CRI の RunPodSandbox を containerd に送る

プラグインを exec するのは containerd です。kubelet は CRI で Pod の作成を頼み、結果の IP を containerd から受け取ります。

ノード間をどう繋ぐか

3 つの仕事のうち、CNI プラグインごとに最も性格が出るのが「ノード間の疎通」です。 第 3 章の例をもう一度使います。 Node 1 (10.0.1.11) の Pod 10.244.0.23 が、Node 2 (10.0.1.12) の Pod 10.244.1.5 へ送るとき、ノード間のスイッチやルーターは 10.244.1.5 がどこにあるかを知りません。 この宛先をどう届けるかの違いが、次の 3 方式です。

ノード間を繋ぐ 3 つの方式: オーバーレイ (VXLAN)、BGP、クラウドの VPC ルートVXLAN は Pod のパケットを UDP 8472 で包んでノード IP 宛てに送る。BGP はノード同士が Pod CIDR の経路を交換する。VPC ルートはクラウドのルートテーブルに Pod CIDR → ノードを書く。オーバーレイ (VXLAN / Geneve) — この演習クラスタweb10.244.1.5cilium_vxlanUDP 8472[ノード IP ヘッダ][VXLAN][Pod IP ヘッダ][データ]物理網は Pod CIDR を知らなくてよい。MTU が 50 バイト減る。BGP (Calico、Cilium BGP control plane)Node A10.244.1.0/24Node B10.244.2.0/24ToR / ルーターBGP peer経路広告物理網が「10.244.2.0/24 は Node B」を知る。カプセル化なし。クラウドの VPC ルート (GKE の routes-based クラスタ、CCM の Route controller)VPC ルートテーブル10.244.2.0/24 → eni-of-node-bNode B10.244.2.0/24クラウドが転送
図 3オーバーレイ (VXLAN) はパケットを包んでノード IP 宛てに送る。BGP と VPC ルートは物理網に Pod CIDR の経路を教える。
  • オーバーレイ (VXLAN / Geneve):Pod のパケットをまるごと UDP パケット (VXLAN は 8472 番、Geneve は 6081 番) の中身にして、相手ノードの IP 宛てに送る。相手ノードが外箱を外して Pod に渡す。Cilium のドキュメントは「ノード同士が IP/UDP で届きさえすればよく、ノード間のネットワークは Pod CIDR を知らなくてよい」と利点を挙げ、代償として VXLAN で 1 パケットあたり 50 バイトの MTU の減少を挙げている。Flannel のドキュメントが推奨する方式であり、Cilium の既定 (routing-mode: tunnel) でもある。この演習クラスタもこれです。
  • BGP:各ノードが「10.244.1.0/24 は私のところ」と、BGP (ルーター同士が経路を教え合うプロトコル) でスイッチやルーターに伝える。Calico のドキュメントは、ノード同士が BGP で経路を交換する full-mesh を既定とし、オンプレでは ToR ルーターと直接 peer する構成を説明している。包まないので速いが、ノード間のネットワークが Pod CIDR をルーティングできる必要がある (Cilium の native routing の要件)。
  • クラウドの VPC ルート:VPC のルートテーブルに「Pod CIDR → そのノード」を書く。Cilium のドキュメントは「ネットワーク上に全 Pod への経路を知るルーターがある」形としてクラウド連携を挙げている。経路を書く役は第 9 章で扱う cloud-controller-manager の Route controller。

もう 1 つ、Pod に VPC のアドレスそのものを配ってしまう方式があります。

Amazon VPC CNI: ENI のセカンダリ IP を Pod にそのまま渡すEC2 ノードに複数の ENI が付き、各 ENI のセカンダリ IP を ipamd がウォームプールとして確保する。Pod はその VPC アドレスを直接もらうので、Pod IP は VPC の中でそのまま到達できる。VPC 10.0.0.0/16 (サブネット 10.0.1.0/24)EC2 ノード (c5.large: ENI 3 枚 × 10 IP)10.0.1.10eth0 (primary ENI)10.0.1.10 + 9 secondaryeth1 (secondary ENI)10 secondary IPseth2 (warm ENI)WARM_ENI_TARGET=1web10.0.1.23api10.0.1.31db10.0.1.57aws-node (ipamd)L-IPAM: ウォームプール管理EC2 API で ENI 追加
図 4Amazon VPC CNI。ENI のセカンダリ IP を ipamd がプールし、Pod にそのまま渡す。Pod IP は VPC の中でそのまま到達できる。

Amazon VPC CNI は、EC2 ノードに付いた ENI のセカンダリ IP を Pod に渡します。 AWS のドキュメントは「Pod は VPC ネットワーク上と同じ IP アドレスを持てる」と説明し、ノード上の aws-node (ipamd) が ENI と空き IP をウォームプールとして先に確保し、足りなくなると ENI を追加する、と書いています。 Pod の IP は VPC のアドレスなので、包む必要も経路を教える必要もありません。 代わりに、インスタンスタイプごとの ENI 数と IP 数が Pod 数の上限になり、AWS のドキュメントは上限を (ENI 数 × (ENI あたりの IP 数 - 1)) + 2 としています (-1 は各 ENI の primary IP、+2 はホストのネットワークを使う kube-proxy と VPC CNI 自身の分)。 Pod が消えた IP は 30 秒の冷却期間を置いてから再利用され、その間に各ノードの kube-proxy が書き換え表の更新を終えます。 この方式が第 8 章の「コンテナネイティブ LB」の土台です。

代表的な CNI プラグイン

ノード間の繋ぎ方で CNI プラグインの性格が決まる
観点方式特徴
FlannelVXLAN (推奨)ホストごとにサブネットを貸し出す L3 ネットワーク。NetworkPolicy は自前で強制せず、Calico などと組む
CalicoBGP / IPIP / VXLAN / eBPFノード間の BGP full-mesh が既定。ToR と peer する構成や、BGP が使えない環境向けの IPIP / VXLAN もある
CiliumeBPF (VXLAN / Geneve / native)kube-proxy の代替、NetworkPolicy、Gateway API まで一式。GKE Dataplane V2 の中身
Amazon VPC CNIENI のセカンダリ IPEKS の EC2 ノードで既定の CNI。Pod が VPC アドレスを持つ

同じ OSS でも、クラウドで提供される範囲は違う

GKE Dataplane V2 は Cilium を使って Service を実装し、AKS には Azure CNI Powered by Cilium があります。 ただし、マネージドサービスの採用を、上流 Cilium の全機能を自由に使えるという意味に読むことはできません。 たとえば Google は、GKE Dataplane V2 と kube-proxy の間で Service の新機能の対応時期が異なることを明記しています。

EKS でもノードの種類によって違います。 AWS は Hybrid Nodes 向けに Cilium をサポートし、このノードでは Amazon VPC CNI を使えません。 EC2 ノードの既定と Hybrid Nodes の選択肢を混ぜずに確認します。

Calico も eBPF データプレーンを提供しています。 「Calico は BGP、Cilium は eBPF」とだけ分けると、経路を配る方法と、パケットを処理する方法が混ざります。 クラウドの対応範囲、IP の割り当て、経路の配布、Service と NetworkPolicy の実装を分けて比較すると、製品名だけでは見えない違いを確認できます。

ノードの中で読む

ここからはノードの中です。

ノードの中に入るノードの中で実行
kubectl debug node/$(kubectl get node -o jsonpath='{.items[0].metadata.name}') -it \
  --profile=sysadmin --image=busybox:1.37 -- chroot /host nsenter -t 1 -m -u -i -n -p -- bash

抜けるときは exit。デバッグ用 Pod は自動では消えないので、終わったら kubectl delete pod -l app.kubernetes.io/managed-by=kubectl-debug かkubectl get pods で確認して消してください。

まず、containerd が読む conflist と、プラグインの実行ファイルを確認します。

CNI の設定とプラグインを見るノードの中で実行
ls -la /etc/cni/net.d/ /opt/cni/bin/
cat /etc/cni/net.d/05-cilium.conflist

出力例

/etc/cni/net.d/:
-rw-r--r-- 1 root root  ... 05-cilium.conflist
/opt/cni/bin/:
-rwxr-xr-x 1 root root  ... cilium-cni
-rwxr-xr-x 1 root root  ... loopback
...
{
  "cniVersion": "0.3.1",
  "name": "cilium",
  "plugins": [
    {
      "type": "cilium-cni",
      "enable-debug": false,
      "log-file": "/var/run/cilium/cilium-cni.log"
    }
  ]
}

plugins に cilium-cni が 1 つあるだけです。 IPAM の設定が書かれていないのは、cilium-cni が cilium-agent に IP を頼むからです。 ファイル名の 05- は、名前順で先に選ばれるようにするための接頭辞です。

次に、cilium-agent が報告する状態を見ます。 cilium コマンドは cilium-agent の Pod の中にあり、ノードのシェルからは kubectl が使えないので、ここは Web ターミナル側で打ちます。

Cilium の状態を見る (Web ターミナル側)
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium status | head -20
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium status --verbose | grep -A3 "^IPAM"

出力例

KVStore:                 Disabled
Kubernetes:              Ok   1.37 (v1.37.1) [linux/amd64]
Kubernetes APIs:         ["EndpointSliceOrEndpoints", "cilium/v2::CiliumNetworkPolicy", ...]
KubeProxyReplacement:    True   [eth0 ...]
Host firewall:           Disabled
CNI Chaining:            none
CNI Config file:         successfully wrote CNI configuration file to /host/etc/cni/net.d/05-cilium.conflist
Cilium:                  Ok   1.20.2 (v1.20.2-...)
IPAM:                    IPv4: 9/254 allocated from 10.244.0.0/24
...
IPAM:                   IPv4: 9/254 allocated from 10.244.0.0/24
Allocated addresses:
  10.244.0.23 (default/web-7c9d...)

IPAM: … allocated from 10.244.0.0/24 は、このノードの Pod に払い出せる 254 個の IP のうち 9 個が使用中という意味です。 Cilium の IPAM には複数のモードがあり (既定は Cilium 自身が CIDR を管理する cluster-pool)、演習クラスタは ipam: kubernetes です。 Cilium のドキュメントによれば、このモードでは「Kubernetes が各 Node に割り当てた PodCIDR の範囲から IP を払い出す」ので、kube-controller-manager を --allocate-node-cidrs 付きで動かす必要があります。

Node の podCIDR と一致することを確認する
kubectl get node -o jsonpath='{.items[0].spec.podCIDR}{"\n"}'

最後に、ノード間の疎通を担う VXLAN デバイスと、Cilium が置いた経路です。 単一ノードなので相手は居ませんが、トンネルの口は開いています。

VXLAN デバイスと Pod 向けの経路を見るノードの中で実行
ip -d link show cilium_vxlan
ip addr show cilium_host
ip route | grep -E "10\.244|cilium"

出力例

5: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    vxlan external id 0 ... dstport 8472 ...
6: cilium_host@cilium_net: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 10.244.0.87/32 scope global cilium_host
10.244.0.0/24 via 10.244.0.87 dev cilium_host proto kernel src 10.244.0.87 mtu 1450

cilium_vxlan の dstport 8472 が VXLAN のポート、経路の mtu 1450 が 50 バイト分のカプセル化の跡です。 cilium_host の IP (ここでは 10.244.0.87) は、第 2 章で Pod のデフォルトルートの先に見えた IP です。 Cilium のドキュメントは「Pod の network namespace には、ノードの router IP を指すデフォルトルートがあり、その先は Pod 内で eth0、ホスト側で lxcXXXXXX と名付けられた veth ペア」と説明しています。 Pod から出たパケットは veth のホスト側 (lxc…) の tc ingress hook で eBPF プログラムに拾われ、同じノードの Pod ならそのまま相手の veth へ、別ノードの Pod なら cilium_vxlan で UDP に包んで相手ノードへ送られます。

Pod の veth とホスト側の対応を見るノードの中で実行
ip link show type veth | grep -E "^[0-9]+: lxc" | head -5

ノードから抜けたら、デバッグ用の Pod を消しておきます。

デバッグ Pod を消す
kubectl delete $(kubectl get pods -o name | grep node-debugger) --wait=false

ふりかえり

QCNI プラグインを exec するのは誰?

kubelet は CRI で RunPodSandbox を頼むだけです。containerd が netns を作り、conflist を読み、環境変数と stdin の JSON 付きでプラグインを exec します。CNI の仕様も「ランタイムはプラグインを呼ぶ前に netns を作る」と定めています。

QVXLAN オーバーレイの利点として正しいものは?

Pod のパケットをノード IP 宛ての UDP で包むので、ノード間のネットワークは Pod CIDR の存在を知らなくて済みます。代償はヘッダ分 (VXLAN で 50 バイト) の MTU の減少です。

Qこの演習クラスタで Pod の IP はどの範囲から払い出される?

ipam: kubernetes モードでは、kube-controller-manager が –allocate-node-cidrs で各 Node に配った podCIDR を cilium-agent が使います。cilium status の IPAM 行と kubectl get node の spec.podCIDR が一致します。

参考