DockerとKubernetesのPodのネットワーキングについてまとめました
KubernetesにおけるPod内/Pod間の通信について、本記事にまとめました。
次の公式ドキュメントを参考にしています。
Podとは
KubernetesにおけるPodは、コンテナを管理する最小単位のリソースであり、 Podは1つ以上のコンテナで構成されます。 Pod内のコンテナは、一連のnamespaceを共有しています。

KubernetesのPodは、次のような観点での下位互換性を維持するように、 ネットワーキング機能が実装されています。
そのため、Podは仮想マシンや物理ホストのように扱うことができます。
クラスタの観点からは、Podはどのノードで起動していても他のPodと通信できる必要があります。 Kubernetesの実装では、Pod間通信のためにPod間に明示的にリンクを作成する必要はなく、 コンテナとホストのポート番号のマッピングを扱う必要もありません。
Docker bridge ネットワーキング
Pod間通信の前に、まずは基本となるDockerの通信について見ていきます。

Dockerにおけるデフォルトの birdge ネットワーキングでは、 docker0 という名前の仮想ブリッジを作成し、
そのブリッジに対してプライベートネットワークのサブネットを割り当てます。
コンテナごとに、コンテナ内で eth0 と表示されているveth (Linuxにおける仮想L2インタフェース) を使います。
vethは次のようなペアで作成されます。
eth0:コンテナのnetwork napespace用vethXXXXXXX:ホストのnetwork namespace用
ホスト側の vethXXXXXXX は docker0 にブリッジングされます。
そして、コンテナ側の eth0 には、 docker0 に割り当てられているサブネットの範囲からIPアドレスが割り当てられます。
docker0 とホストのインタフェース (図では ens160 ) の間は、IPマスカレードによってIPアドレスとポート番号が変換されて転送されます。
DockerのIPマスカレードについては次の記事が詳しいです。
作成されたコンテナは、同じホスト上にある場合のみ、他のコンテナと通信できます。 異なるホスト上のコンテナ同士は疎通性がありません。
そのため、コンテナがノード間で通信できるようにするには、ホストのIPアドレスにポート番号が割り当てられている必要があります。 このポート番号がコンテナに転送またはプロキシされます。 ですので、コンテナが使用するポート番号が重複しないよう調整したり、ポート番号を動的に割り当てたりする必要があります。
Kubernetesのネットワーク実装の基本要件
コンテナが利用するポート番号の調整は困難ですし、ポート番号の動的な割り当てはシステムを複雑化させてしまいます。 それを踏まえ、Kubernetesにおけるネットワーク実装は、次の基本要件を満たすことで複雑さを減らし、仮想マシンからコンテナへのアプリケーション移植性を高めています。
- 全てのコンテナはNATなしで他の全てのコンテナと通信可能
- 全てのノードはNATなしで全てのコンテナと通信可能 (逆も可)
コンテナが使用するIPアドレスと他のユーザから見えるコンテナのIPアドレスは同一
アプリケーションを仮想マシンで実行している場合、仮想マシンのIPアドレスを使って他の仮想マシンと通信していますが、 Kubernetesではそれと同じことをコンテナで実現します。
Pod内ネットワーキングとPodの実装
KubernetesのPod内のコンテナ間のネットワーキングとPodの実装について見ていきます。

KubernetesはPodにIPアドレスを割り当てる、 IP-per-Pod モデルを採用しています。
Pod内のコンテナは、それらのIPアドレスが割り当てられたインタフェースを含む、
network namespaceを共有します。
そのため、Pod内のコンテナ同士は localhost 上の互いのポート番号に対して通信できます。
Pod内の複数コンテナはnetwork namespaceを共有しているため、重複するポート番号を使用することはできません。
ホストで利用するプロセスのように、Pod内のコンテナのポート番号が重複しないようにする必要があります。
IP-per-podモデルは、Dockerの機能により実装されています。
Podコンテナがnetwork namespaceを作成し、ユーザが作成するPod内のアプリケーションコンテナはそのnetwork namespaceに参加します。
Dockerでは --net=container:<id> オプションにより、指定するコンテナと同一のnetwork namespaceに加わります。
では、実際にPodをデプロイして確認してみます。
sample-pod.yaml を作成し、Podに2台のコンテナを作成してみます。
sample-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: sample-pod
spec:
containers:
- name: busybox-01
image: busybox:1.30
command: ["sleep", "3600"]
- name: busybox-02
image: busybox:1.30
command: ["sleep", "3600"]
$ kubectl apply -f sample-pod.yaml
クラスタノード上で作成されたPodとその内部のコンテナを確認してみます。
k8s-node:~$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 575ffdf9c5d3 3a093384ac30 "sleep 3600" 32 minutes ago Up 32 minutes k8s_busybox-02_sample-pod_default_8e79fad5-17c7-11e9-87b0-000c29cd2791_7 31381a2cd6a7 3a093384ac30 "sleep 3600" 32 minutes ago Up 32 minutes k8s_busybox-01_sample-pod_default_8e79fad5-17c7-11e9-87b0-000c29cd2791_7 ba0d8b12e056 k8s.gcr.io/pause:3.1 "/pause" 8 hours ago Up 8 hours k8s_POD_sample-pod_default_8e79fad5-17c7-11e9-87b0-000c29cd2791_0

明示的に作成したBusyBoxコンテナ2台のほかに、 pauseコンテナが自動で作成されています。
このpauseコンテナが、2台のコンテナに対してnetwork namespaceを提供しています。
pauseコンテナは、あくまでnetwork namespaceを提供するだけであり、
SIGINT 、 SIGTERM 、 SIGCHLD が実行されるまで
pause() を実行するだけの実装です。
それぞれのコンテナでインタフェースを確認すると、 eth0 に
が共通で割り当てられていることがわかります。
k8s-node:~$ docker exec k8s_busybox-01_sample-pod_default_8e79fad5-17c7-11e9-87b0-000c29cd2791_5 ip address show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 0a:58:0a:00:02:03 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.3/24 scope global eth0
valid_lft forever preferred_lft forever
k8s-node:~$ docker exec k8s_busybox-02_sample-pod_default_8e79fad5-17c7-11e9-87b0-000c29cd2791_5 ip address show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 0a:58:0a:00:02:03 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.3/24 scope global eth0
valid_lft forever preferred_lft forever
k8s-node:~$ bridge link show
8: vethcd95a6f2 state UP @ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 master cni0 state forwarding priority 32 cost 2
余談ですが、Dockerと同様に、Podでもホストのポート番号を利用可能です。
その場合にはPodに hostPort を設定しますが、Podをスケジューリングできる場所が制限されてしまいます。
Podのポートを明示的に外部に公開する場合は、 hostPortの前に nodePortの利用が推奨されます (参考)。
Pod間ネットワーキング
前述したKubernetesのネットワーク実装の基本要件
- 全てのコンテナはNATなしで他の全てのコンテナと通信可能
- 全てのノードはNATなしで全てのコンテナと通信可能 (逆も可)
コンテナが使用するIPアドレスと他のユーザから見えるコンテナのIPアドレスは同一
を満足するように、様々なKubernetesのネットワーキングモデルが実装されています。これらを利用することで、Pod間の通信が可能となります。
- Cisco ACI (Application Centric Infrastructure)
- AOS from Apstra
- Big Cloud Fabric from Big Switch Networks
- Cilium
- CNI-Genie from Huawei
- cni-ipvlan-vpc-k8s
- Contiv
- Contrails / Tungten Fabric
- DANM
- flannel
- GCE (Google Compute Engine)
- Jaguar
- Knitter
- Kube-router
- L2 networks and linux bridging
- Multus (a Multi Network plugin)
- VMware NSX-T
- Nuage Networks VCS (Virtualized Cloud Services)
- OpenVSwitch
- OVN (Open Virtual Networking)
- Project Calico
- Romana
- Weave Net from Weaveworks
個人的には、flannelやProject Calicoの採用事例を多く見かける気がします。 GithubのStar数については次の記事が参考になります。
flannelはL2 over L3ネットワーキング (デフォルトではVXLAN) で、CalicoではBGPによるピュアL3ネットワーキングで、 Pod間の到達性を確保します。
本記事では最もシンプルな実装であるflannelを動かしてみます。
flannelによるPod間ネットワーキング
まず、Kubernetesクラスタにて、flannelをデプロイしておきます。
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
そして、同一クラスタ内の2台のクラスタノード上にPodを1台ずつデプロイします。
nodeSelector で明示的にデプロイ先を指定しておきました。
pod-01用 ( pod-01.yaml )
apiVersion: v1 kind: Pod metadata: name: pod-01 spec: containers: - name: ubuntu-01 image: borkmann/misc command: ["sleep", "3600"] nodeSelector: name: node-01
pod-02用 ( pod-02.yaml )
apiVersion: v1 kind: Pod metadata: name: pod-02 spec: containers: - name: ubuntu-02 image: borkmann/misc command: ["sleep", "3600"] nodeSelector: name: node-02
$ kubectl apply -f pod-01.yaml $ kubectl apply -f pod-02.yaml

flannelをデプロイすると、図のように、 cni0 と flannel.1 というインタフェースが作成されます。
cni0:bridgeインタフェースflannel.1:VTEP (VXLAN Tunnel End Point) インタフェース
クラスタノード node-01 にて、 flannel.1 インタフェースを確認してみます。
node-01:~ $ ip -4 --detail address show dev flannel.1
15: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 06:57:e6:f4:23:41 brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 192.168.20.11 dev ens160 srcport 0 0 dstport 8472 nolearning ttl inherit ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 10.0.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
flannel.1 にはVNI (VXLAN Network Identifier) 1が割り当てられており、ホストのインタフェース ( ens160 ) に紐付いています。
VXLANでカプセル化されたパケットは、 ens160 から送出されます。
次に、ARPテーブルを確認してみます。
node-01:~$ ip neigh | grep flannel.1 10.0.2.0 dev flannel.1 lladdr 9a:61:55:41:8d:06 PERMANENT
対向のPodが起動しているクラスタノードの flannel.1 のMACアドレスに対するエントリが存在することが確認できます。
これによりPod間の通信が可能となっています。
node-02:~ $ ip -4 --detail address show dev flannel.1
14: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 9a:61:55:41:8d:06 brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 192.168.20.12 dev ens160 srcport 0 0 dstport 8472 nolearning ttl inherit ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 10.0.2.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
簡単にですが、flannelを動かしてPod間ネットワーキングについて確認しました。 次回があればCalicoを動かしてみたいと思います。
2019/04/17 追記
Calicoを動かした結果を次の記事にまとめました。