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

Docker なしでコンテナのネットワークを作る

分離したネットワークを作り、仮想のケーブルでつなぎます。設定を足すたびに通信がどう変わるか確かめます。

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

同じ Linux マシンでも、コンテナの中とホストでは、見えるネットワーク接続口が違います。 これは Linux が、接続口や IP アドレスなどの設定を、別々に持てるためです。

この章では、その分かれたネットワークを 2 つ作り、仮想のケーブルでつなぎます。 最初は届かなかった ping が、設定を足すと届くようになる。 その変化を確かめながら、コンテナのネットワークを手で組み立てます。

GOAL この章のゴール

  • 分離したネットワークを作り、互いに通信させられる
  • 仮想のスイッチを使い、複数のネットワークをつなげられる
  • 外部へ通信するための設定を加え、動作を確かめられる

コンテナごとにネットワークの見え方が違う理由

コンテナの中で ip addr を実行すると、そのコンテナ用の接続口が表示されます。 同じコマンドをホストで実行しても、一覧は同じになりません。 Linux がネットワークの設定一式を分けて持っているからです。

この区切りが network namespaceネットワークネームスペースネットワークの接続口、IP アドレス、経路などの設定を分けて持つ Linux の仕組み。netns と略す。用語集で見る です。 プロセスはどれか 1 つに属し、その中の接続口を使います。 Docker の通常のブリッジ構成では、コンテナ用の netns に eth0 と lo を用意します。

別々の netns を通信させるには、間をつなぐ必要があります。 これから作るのは、2 つの netns に仮想ケーブルの両端を 1 つずつ入れる構成です。

ここから先は、次の手順で組み立てます。

  1. netns を 2 つ作り、veth でつないで ping する。
  2. ブリッジを足して 3 つの netns をつなぎ、docker0 と同じ形にする。
  3. nft で送信元アドレスを書き換え、外へ出られるようにする (前の章の masquerading です)。
  4. ノードで動いている本物の Pod の netns を探す。

ノードの中に入る

この章はノード (KubeVirt VM) の中で root として作業します。

ノードの中に入るノードの中で実行
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 で確認して消してください。

1. netns を 2 つ作って直結する

ip netns add で netns を作ると、NIC が lo しかない空っぽのネットワークの世界が 1 つできます。 ip-netns(8) によれば、名前付きの netns は /var/run/netns/<名前> のファイルとして置かれ、以降の ip netns exec はこのファイルを開いてその netns の中でコマンドを実行します。

2 つの network namespace を veth ペアで直結するred と blue という 2 つの netns があり、veth-red と veth-blue のペアで繋がっている。それぞれ 10.10.0.1 と 10.10.0.2 を持つ。ノード (ホストの netns)netns: redveth-red10.10.0.1/24netns: blueveth-blue10.10.0.2/24veth ペア (ケーブル 1 本)ip link add veth-red type veth peer name veth-blue
図 1red と blue の 2 つの netns を、veth ペア 1 組で直結する。
netns を 2 つ作り、veth ペアで繋ぐノードの中で実行
ip netns add red
ip netns add blue
ip link add veth-red type veth peer name veth-blue
ip link set veth-red netns red
ip link set veth-blue netns blue
ip -n red addr add 10.10.0.1/24 dev veth-red
ip -n blue addr add 10.10.0.2/24 dev veth-blue
ip -n red link set lo up; ip -n red link set veth-red up
ip -n blue link set lo up; ip -n blue link set veth-blue up
ip netns exec red ping -c 2 10.10.0.2

出力例

PING 10.10.0.2 (10.10.0.2) 56(84) bytes of data.
64 bytes from 10.10.0.2: icmp_seq=1 ttl=64 time=0.08 ms
64 bytes from 10.10.0.2: icmp_seq=2 ttl=64 time=0.05 ms

ip link add … type veth peer name … は vethブイイーサ仮想イーサネットのペア。片方に入ったパケットがもう片方から出てくる、ケーブル 1 本相当の仮想デバイス。用語集で見る を 1 組作ります。 veth(4) は「veth デバイスは必ず相互に接続されたペアとして作られ、片方で送信したパケットは即座にもう片方で受信される」と説明しています。 作った直後は両端ともホストの netns にあり、ip link set <dev> netns <名前> で片方ずつ別の世界へ移します。 ip -n red … は ip netns exec red ip … の省略形です。

veth-red はホストの netns から red へ移ったので、ホスト側で ip link を打っても一覧に出ません。 NIC は同時に 1 つの netns にしか属せないからです。 コンテナの中から eth0 が 1 枚見えるだけなのも、同じ理由です。

2. ブリッジで 3 つ繋ぐ (docker0 を手で作る)

veth ペアはケーブル 1 本なので、直結できるのは 2 つの netns までです。 3 つ以上を繋ぐには、真ん中にスイッチが要ります。 Linux ブリッジがそれです。

3 つの netns を Linux ブリッジで繋ぐ: docker0 を手で作ったものred / blue / green の 3 つの netns が、それぞれ veth ペアで br-lab というブリッジに繋がる。ブリッジ自身は 10.10.1.1 を持ちゲートウェイになる。ノード (ホストの netns)netns: redeth010.10.1.2/24netns: blueeth010.10.1.3/24netns: greeneth010.10.1.4/24vethvethvethbr-lab (Linux bridge) 10.10.1.1/24 = default gatewayip link add br-lab type bridge / ip link set veth-xxx master br-lab
図 2br-lab というブリッジに、3 つの netns を veth で接続する。ブリッジに IP を持たせてゲートウェイにする。

さっきの直結ペアは一度消して、ブリッジ構成を組み直します。

ブリッジを作り、3 つの netns を繋ぐノードの中で実行
ip netns del red; ip netns del blue
ip link add br-lab type bridge
ip addr add 10.10.1.1/24 dev br-lab
ip link set br-lab up
for i in 2 3 4; do
  n=$(printf 'red\nblue\ngreen' | sed -n "$((i-1))p")
  ip netns add "$n"
  ip link add "veth-$n" type veth peer name "eth0" netns "$n"
  ip link set "veth-$n" master br-lab up
  ip -n "$n" addr add "10.10.1.$i/24" dev eth0
  ip -n "$n" link set lo up; ip -n "$n" link set eth0 up
  ip -n "$n" route add default via 10.10.1.1
done
ip netns exec red ping -c 1 10.10.1.3
ip netns exec green ping -c 1 10.10.1.2
bridge link show br-lab

出力例

64 bytes from 10.10.1.3: icmp_seq=1 ttl=64 time=0.06 ms
64 bytes from 10.10.1.2: icmp_seq=1 ttl=64 time=0.06 ms
12: veth-red@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br-lab state forwarding ...
13: veth-blue@if2: ... master br-lab ...
14: veth-green@if2: ... master br-lab ...

bridge link show は、ブリッジにつながったポート (ここでは 3 本の veth のホスト側) とその状態を表示します。 この手順は、Docker が docker0 と veth で毎回やっていることと同じです。 違いはブリッジの名前と IP 帯だけです。 peer name eth0 netns red のように、作った瞬間に片方を別の netns へ送ることもできます。 コンテナの中の NIC がどれも eth0 という名前なのは、こうして netns ごとに名前の空間が分かれているからです。

3. nft で外に出す

netns の中から外のネットワークへ ping してみます。 この時点では失敗します。

外へ出てみる (まだ失敗する)ノードの中で実行
GW=$(ip route | awk '/^default/ {print $3; exit}')
echo "gateway: $GW"
ip netns exec red ping -c 1 -W 1 "$GW" || echo "届かない"

パケットは red → br-lab → ノードの eth0 → ゲートウェイと進みますが、送信元が 10.10.1.2 のままです。 ゲートウェイは 10.10.1.0/24 への戻り経路を知らないので、返事が帰ってきません。 この戻り経路の問題を解くのが NAT です。

netns から外へ出る: IP forwarding と nft masqueradered netns からブリッジ、ホストの eth0 を経由して外のゲートウェイへ。postrouting の masquerade で送信元が 10.10.1.2 からノードの IP に書き換わる。ノードnetns: redsrc 10.10.1.2br-lab10.10.1.1eth0 (ノード)10.0.1.11ゲートウェイ10.0.1.1ip_forward=1nft: postrouting hook → masqueradesrc 10.10.1.2 → src 10.0.1.11 に書き換えて出す (戻りは conntrack が逆変換)
図 3postrouting hook に masquerade を置くと、ノードの eth0 から出るときに送信元がノードの IP に書き換わる。

nftablesエヌエフテーブルズLinux カーネルの netfilter にルールを入れる現在の標準フレームワークと、その CLI nft。iptables の後継で、テーブル / チェーン / ルールに加えてセットやマップ (vmap) を持つ。用語集で見る でルールを入れます。 nft(8) の用語では、テーブルはチェーンの入れ物、チェーンのうち base chain はカーネルのネットワークスタックからパケットが入ってくる入口 (hook) に結びついたもので、ルールはその中に並びます。 手順は「テーブルを作る → hook に繋がったチェーンを作る → ルールを足す」の 3 段です。

nft で masquerade を入れるノードの中で実行
sysctl -w net.ipv4.ip_forward=1
nft add table ip lab
nft 'add chain ip lab post { type nat hook postrouting priority srcnat; policy accept; }'
nft add rule ip lab post ip saddr 10.10.1.0/24 oifname != "br-lab" masquerade
nft list table ip lab
ip netns exec red ping -c 2 "$GW"

出力例

table ip lab {
	chain post {
		type nat hook postrouting priority srcnat; policy accept;
		ip saddr 10.10.1.0/24 oifname != "br-lab" masquerade
	}
}
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.4 ms

ip_forward=1 はノードがルーターとして転送してよいという宣言です (Kubernetes のノードでは最初から 1 になっています)。 postrouting はシステムから出ていく全パケットが通る hook で、srcnat はその hook での標準の優先度の名前です。 masquerade は nft(8) が「SNAT の特殊形で、送信元アドレスを出力インターフェースのアドレスにする」と説明する動作です。 nat 型のチェーンを通るのは接続の最初のパケットだけで、以降のパケットと戻りパケットは conntrackコントラックnetfilter の接続追跡。NAT で書き換えた通信の対応表を持ち、戻りパケットを自動で逆変換する。用語集で見る の対応表に従って書き換えられます。 前の章の -p による宛先の書き換え (DNAT) も、Kubernetes の NodePort で外から入った通信の送信元書き換えも、この masquerade と同じく netfilter の NAT で行われます。

4. 本物の Pod の netns を見る

ノードで動いている Pod も、同じ道具でできています。 containerd が Pod ごとに netns を作り、/run/netns/cni-<uuid> という名前を付けるので、ip netns list に並びます。

手で作った netns と、Cilium が作る Pod の netns は同じ構造左は手作業の red netns と veth。右は containerd が作った Pod の netns (cni-xxxx) に Cilium が差した veth (lxcxxxx)。ホスト側の片割れは Cilium ではブリッジに繋がず、eBPF プログラムが処理する。手作業 (この章)netns: redeth0 (veth)10.10.1.2br-labCilium が作る Pod (第 4 章)netns: cni-7f3a… (containerd が作成)eth0 (veth)10.244.0.23/32lxc7f3a…eBPF (tc) が転送先を決める
図 4手で作った netns と、Cilium が作った Pod の netns。構造は同じで、ホスト側の処理がブリッジか eBPF かだけが違う。
Pod の netns と veth を探すノードの中で実行
ip netns list | head
NS=$(ip netns list | awk '/cni-/ {print $1; exit}')
ip -n "$NS" addr show eth0
ip -n "$NS" route
ip link show type veth | grep -c lxc

出力例

cni-3f1e8a2b-... (id: 3)
cni-9c0d44aa-... (id: 2)
...
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
    inet 10.244.0.23/32 scope global eth0
default via 10.244.0.87 dev eth0 mtu 1450
10.244.0.87 dev eth0 scope link
7

Pod の eth0 も veth で、ホスト側の片割れは lxc で始まる名前です (Cilium の流儀)。 このクラスタのネットワークプラグインである Cilium (第 4 章で扱います) は、その片割れに eBPF プログラム (カーネルの中で動かす小さなプログラム) を取り付け、パケットが出てきた時点で行き先を決めます。 ブリッジには接続しません。 Pod の IP が /32 なのは、同じサブネットの隣人をブリッジ経由で探す構成ではないからです。 デフォルトルートの via 10.244.0.87 はホスト側にある cilium_host という NIC の IP で、Pod はこの IP 宛てに ARP を打ち、Cilium が代わりに答えます (ブリッジ構成なら、ここにブリッジの IP が入ります)。 この続きは第 4 章で扱います。

片付け

作ったものを全部消すノードの中で実行
for n in red blue green; do ip netns del "$n" 2>/dev/null; done
ip link del br-lab 2>/dev/null
nft delete table ip lab 2>/dev/null
ip netns list | grep -v cni- || echo "clean"
exit

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

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

ふりかえり

Qip link set veth-red netns red を実行した後、ホストの netns で ip link を打つとどうなる?

NIC は同時に 1 つの netns にしか属せません。red に移した veth-red はホスト側からは消え、ペアの veth-blue はまだホストに残っています。

Qmasquerade を入れる前に netns から外へ ping が通らなかった理由は?

行きのパケットは届きます。戻りの宛先 10.10.1.2 がゲートウェイから見て未知のネットワークなので返事が帰れません。masquerade で送信元をノードの IP に書き換えると、戻りはノードに届き、conntrack が元に戻します。

QCilium の Pod で eth0 の IP が /32 なのはなぜ?

ブリッジ構成では、同じブリッジにつながった相手と直接話せるよう、/24 などのサブネットを持たせます。Cilium では veth のホスト側で eBPF がパケットを拾って転送するので、隣人を探す必要が無く、/32 と「デフォルトルートはホスト側の IP」という設定で足ります。

参考