Web アプリとデータベースが別々のノードに置かれても、通信は届いてほしい。 Kubernetes では、Pod がどのノードで動くかにかかわらず、接続できるネットワークが必要です。
前の章までは、1 台のホストの中の配線を見ました。 今度はノード間をつなぎ、Pod の IP アドレスまで通信が届く経路を追います。 ノード、Pod、Service に付く IP の役割の違いも、ここで整理します。
GOAL この章のゴール
- ノードをまたぐ Pod 同士の通信を図で追える
- パケットを包んで運ぶ方法と、経路を知らせる方法を区別できる
- ノード、Pod、Service の IP の役割を見分けられる
Docker のブリッジが 1 台で閉じる理由
第 1 章で見たとおり、docker0 はホストのカーネルの中にあるソフトウェアのスイッチで、コンテナの veth はそこにつながっています。
Docker のドキュメントも、ユーザー定義のブリッジは同じ Docker ホスト上のコンテナ同士をつなぐもので、別のホストのコンテナと通信するには overlay ネットワーク (Swarm) を使う、と説明しています。
冒頭の 2 台の例で、1 台目の web から 172.17.0.3 に送るとします。
1 台目のカーネルは、その宛先が自分の docker0 につながっていると判断して、1 台目の中で配送を終えます。
2 台目にそのアドレスのコンテナが居ても、パケットはホストの外に出ません。
2 台目のコンテナに届けるには、-p 8080:80 で 2 台目のホストの IP とポートを公開し、そこ宛てに送るしかありません。
この宛先はコンテナが入れ替わるたびに変わります。
Kubernetes が Pod のネットワークに求める条件
Kubernetes では、scheduler が Pod をどのノードに置くかを決め、ノードが壊れれば別のノードで作り直します。 置き場所によって通信の仕方が変わると、アプリはノードを意識して書くことになります。 そこで公式ドキュメント「The Kubernetes network model」は、ネットワークに次の条件を課しています。
- Pod ごとにクラスタ内で一意の IP を持つ:Pod は専用の network namespace を 1 つ持ち、Pod 内のコンテナはそれを共有して
localhostで通信できる。 - Pod 同士は NAT 無しで直接届く:同じノードでも別のノードでも、プロキシやアドレス変換を介さずに通信できる。受け取る側から見た送信元は、送った Pod の IP のまま。
- ノード上のエージェントは、そのノードの全 Pod に届く:kubelet などが Pod にヘルスチェックを送れる。
- Service は安定した IP とホスト名を与える:Pod が入れ替わっても変わらない入口になる (次の節で扱います)。
同じドキュメントは、Pod はポートの割り当て、名前付け、サービスディスカバリ、負荷分散の観点で VM や物理ホストのように扱える、とも書いています。
Docker のブリッジは、冒頭の例のように 1 つ目の条件がホストをまたぐと崩れます。
ホストを越えるときは -p による NAT が入るので、2 つ目の条件も満たせません。
ノード間でどう届けるか
条件を満たすには、Pod の IP の範囲 (Pod CIDR) をノードごとに重ならないように分けて配り、ノードをまたぐときも Pod の IP のままパケットを届ける必要があります。
例として、Node 1 (10.0.1.11) が 10.244.0.0/24、Node 2 (10.0.1.12) が 10.244.1.0/24 を持つとします。
Node 1 の Pod 10.244.0.23 が、Node 2 の Pod 10.244.1.5 へ送るパケットを考えます。
ノードの間にあるスイッチやルーターが知っているのはノードの IP (10.0.1.x) だけで、10.244.1.5 をどこへ運べばよいか分かりません。
解決は 2 通りあります。
1 つ目は オーバーレイネットワークオーバーレイネットワークPod のパケットをそのままノード IP 宛ての UDP パケットの中身にして運び、相手ノードで取り出す方式。物理ネットワークからは、ノード同士が UDP で話しているようにしか見えない。用語集で見る です。
Node 1 のカーネルは「10.244.1.0/24 は Node 2 へのトンネルの先」という経路を持っていて、10.244.0.23 → 10.244.1.5 のパケットに VXLAN のヘッダを付け、その外側に 10.0.1.11 → 10.0.1.12、UDP 宛先ポート 8472 の IP ヘッダを付けて送り出します。
途中のネットワークから見えるのは外側のヘッダだけなので、ノード同士の普通の UDP 通信として通ります。
Node 2 は外側のヘッダを外し、中の元のパケットをそのまま Pod 10.244.1.5 に渡します。
中身の送信元と宛先は書き換えられていないので、NAT 無しという条件を満たします。
代償は、外側のヘッダ分だけ 1 パケットに入るデータ量 (MTU) が減ることで、Cilium のドキュメントは VXLAN で 1 パケットあたり 50 バイトと書いています。
2 つ目は、パケットを包まず、ノード間のネットワークに経路を教える方法です。
Node 1 の経路表に「10.244.1.0/24 は 10.0.1.12 へ」と書いておけば、Pod のパケットは Pod の IP のまま Node 2 に届きます。
この経路をルーターに伝える手段が、各ノードが BGP でルーターに知らせる方式と、クラウドの VPC のルートテーブルに「10.244.1.0/24 → Node 2」を書く方式です。
包まないぶん速く、MTU も減りませんが、ノード間のネットワークが Pod CIDR を運べる必要があります。
Cilium のドキュメントは、カプセル化は「ノード同士が IP/UDP で届きさえすればよく、ノード間のネットワークが Pod CIDR を知る必要が無い」、ネイティブルーティングは「ノードをつなぐネットワークが Pod CIDR をルーティングできる必要がある」と対比しています。
どちらの方法でも、ノードごとに Pod CIDR を割り当て、Pod の起動時に IP と経路を設定する仕事が要ります。 公式ドキュメントによれば、この Pod ネットワークを管理するのは Kubernetes 本体の外にある「Pod ネットワークの実装」で、Linux ではコンテナランタイムが CNI という取り決めでそれを呼ぶので、実装は CNI プラグインと呼ばれます (第 4 章)。
| 観点 | Docker | Kubernetes |
|---|---|---|
| 前提 | シングルホスト | 複数ノードの分散システム |
| コンテナの IP | ホスト内だけで有効 (172.17.0.0/16 など) | クラスタ内のどこからでも届く Pod IP |
| 外から入る | ホストのポートに NAT (-p 80:80) | Service (NodePort / LoadBalancer)、Gateway |
| 名前解決 | dockerd 内蔵 DNS (127.0.0.11) | クラスタの DNS (CoreDNS)、svc.cluster.local |
| ノード間の疎通 | 無い (overlay は Swarm 向け) | CNI プラグインの仕事 |
| 内部ロードバランサー | 無い | Service の ClusterIP |
3 つのネットワーク
条件を満たした結果、Kubernetes のクラスタには IP の世界が 3 つ重なります。 公式ドキュメント「Cluster Networking」も、Pod、Service、Node のそれぞれに重ならない IP の範囲を割り当てる必要がある、と書いています。
- Node ネットワーク:物理 (またはクラウドの VPC) の IP。普通のサーバーが繋がるネットワークで、
kubectl get nodes -o wideの INTERNAL-IP がこれです。クラスタを作る前から存在し、Kubernetes はこれをそのまま使います。 - Pod ネットワーク:Pod 1 つに 1 つ付く IP の範囲 (
kubectl get pods -o wideの IP 列)。ノードをまたいでも直接届くように、CNI プラグインが、ノード間の通り道 (VXLAN のトンネル、BGP で配る経路、クラウドのルートテーブル) を用意します。 - Service ネットワーク:Service に付く IP (ClusterIP) の範囲。
10.96.0.10のような IP ですが、この IP を持つ NIC はどのノードにも Pod にもありません。公式ドキュメントは「Pod の IP が固定の宛先に届くのと違い、Service の IP はどのホストも応答しない」と書いています。それでも Pod からcurl http://10.96.0.10/が届くのは、パケットがノードの Linux カーネルを通るときに、宛先が Pod の IP に書き換えられているからです。その書き換えルールを各ノードに入れる仕組み (kube-proxy や Cilium) は第 5 章で扱います。書き換えの対象は Service に書かれたプロトコルとポート (TCP や UDP) だけなので、ICMP のpingは書き換えられず、応答する NIC も無いため返りません。
3 つのうち、Kubernetes 側のコンポーネントが作るのは Pod ネットワークと Service ネットワークの 2 つです。 前者は CNI プラグイン、後者は書き換えルールを入れる仕組みの担当で、それぞれ第 4 章と第 5 章で扱います。
何が変わって、何が変わらないか
3 層に分かれている理由は、変わるものと変わらないものを分離するためです。 Pod は使い捨てで、入れ替わるたびに IP が変わります。 Node が障害を起こせば、その上の Pod は別の Node で作り直され、やはり IP が変わります。 Service の IP だけが変わりません。
下の図で、Pod を入れ替えたり Node B を落としたりしてみてください。
- Service web は 4 つの Pod を向いています
Pod ネットワークの IP は使い捨て、Service ネットワークの IP は安定、Node ネットワークはその土台。3 層の役割分担がこれです。
Service が「向き先」を持ち続けてくれるので、呼ぶ側は Pod の IP を知らなくて済みます。 Docker の内蔵 DNS がやっていた簡易的なサービスディスカバリを、Kubernetes では Service とクラスタの DNS が分散システムの規模でやっている、と捉えると繋がります。
kubectl get nodes -o wide | awk '{print $1, $6}'
kubectl get pods -A -o wide | awk 'NR>1 {print $2, $7, $8}' | head
kubectl get svc -A | head出力例
NAME INTERNAL-IP
tenant-abc-control-plane-xxxx 10.0.1.11
coredns-7b5c4 10.244.0.5 tenant-abc-control-plane-xxxx
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP数字を見るだけで、10.0.1.x は Node、10.244.x.x は Pod、10.96.x.x は Service と分かるようになれば、この章は合格です (CIDR はクラスタごとに違います。自分のクラスタの値は kubectl cluster-info dump | grep -m1 cluster-cidr などで確認できます)。
本物でも「Pod を入れ替えても Service は変わらない」を見ておきます。 CoreDNS の Pod を 1 つ消して、Pod IP と Service IP の前後を比べます。
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide | awk '{print $1, $6}'
kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}{"\n"}'
kubectl -n kube-system delete pod -l k8s-app=kube-dns --wait=false
sleep 10
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide | awk '{print $1, $6}'
kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}{"\n"}'出力例
coredns-7b5c4d9c8-abcde 10.244.0.5
10.96.0.10
coredns-7b5c4d9c8-f9k2x 10.244.0.31
10.96.0.10docker port に何も出ない謎
Docker がコンテナランタイムだった時代のクラスタで、こういう経験をした人がいます。
type: LoadBalancer の Service で公開した Web UI がブラウザで開けるのに、ノードで docker port や docker ps を見ても、そのポートを公開しているコンテナが居ない。
Docker の感覚だと、80 番で開けるなら 0.0.0.0:80->80/tcp のコンテナがあるはずです。
Pod の NIC は CNI プラグインが作った veth で、docker0 には繋がっていません。
公式ドキュメントの言い方では、Pod の network namespace を用意するのはコンテナランタイム (CRI の実装) で、その中の NIC と IP を用意するのは Pod ネットワークの実装 (CNI プラグイン) です。
Docker のポート公開 (-p) はこの経路のどこにも登場しません。
ss -ltnp でも同じことが起きます。
ノードに入った NodePort (たとえば 30080 番) 宛てのパケットは、ノードのカーネルで宛先が Pod の IP とポートに書き換わり、そのまま Pod へ転送されます。
ホストのポートで listen するプロセスが無いので、ss -ltnp に 30080 番は出ません。
ふりかえり
QClusterIP に ping が通らない理由は?
ClusterIP を持つ NIC はどこにもありません。TCP / UDP の宛先としては、ノード上のルール (kube-proxy のルール、または Cilium の eBPF) が Pod の IP に書き換えて届けますが、ICMP echo は書き換えの対象になく、応答する NIC もありません。
QPod ネットワークをノードをまたいで繋ぐのは誰の仕事?
CNI プラグインが Pod に NIC を生やし、IP を配り、ノード間の経路を作ります。kube-proxy は Service の宛先の書き換え、CoreDNS は名前解決です。
QNode が障害を起こして Pod が別の Node で作り直されたとき、変わらないものは?
Pod は新しく作られるので名前も IP も変わります。Service の ClusterIP は Service オブジェクトが消えない限り変わらず、向き先の一覧だけが新しい Pod に更新されます。
参考
- Kubernetes ドキュメント「The Kubernetes network model」(Pod ごとの IP、NAT 無しの疎通、誰が実装するか) https://kubernetes.io/docs/concepts/services-networking/
- Kubernetes ドキュメント「Cluster Networking」(4 つのネットワークの問題、重ならない IP 範囲) https://kubernetes.io/docs/concepts/cluster-administration/networking/
- Kubernetes ドキュメント「Virtual IPs and Service Proxies」(Service の IP はどのホストも応答しない) https://kubernetes.io/docs/reference/networking/virtual-ips/
- Docker ドキュメント「Network drivers」(overlay は複数の Docker デーモン向け) https://docs.docker.com/engine/network/drivers/
- Cilium ドキュメント「Routing」(Encapsulation と Native Routing、VXLAN の 50 バイト) https://docs.cilium.io/en/stable/network/concepts/routing/
- CNCF「Cilium」(2023 年 10 月 Graduated) https://www.cncf.io/projects/cilium/