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を動かした結果を次の記事にまとめました。