MultusによるKubernetesクラスタ外からのパケット転送実験
Multus
最近になってMultusというKubernetes CNIの一つを知った。Kubernetes環境ではPodは通常1つのNICのみを持つが、Multusを使うことでPodに複数のNICを持たせることが可能になる。
- GitHub - intel/multus-cni
- Multusで遊ぶ - 赤帽エンジニアブログ
- Multus CNI pluginをKubernetesで試した - ntoofu
- Adv Network Features in Kubernetes App Note
上記情報源より簡単にその特徴を挙げる。詳細な説明は各記事を参照のこと。(2つの日本語記事はとても分かりやすくまとめられていて助かります。)
- intel社のOSSであり、コンテナ環境でNFVを実現するための課題 (複数NIC、通信の分離や高速化など) を解決する一手法
- Kubernetesで管理されるコンテナがホストサーバーのNICを介して通信高速化機能 (DPDKやSR-IOVなど) を利用することができる
- Multus自体は「Delegating CNIプラグイン」であり、別途CNIプラグインの存在を前提とし、それらに対してネットワーク設定をdelegateする形でNICを設定する
- Multusが作成するNICはKubernetes側では感知されない
- Multusが付与する複数NICの設定はNetworkAttachmentDefinitionというCRDに準拠する
- Multusと同じ「Delegating CNIプラグイン」としてはCNI-Genieがある
- Contrail CNIでも複数のNIC追加ができるがこちらは非Delegatingプラグインらしい
Podに複数のNICを付与する標準はKubernetes Network Plumbing Working Groupで定義されているらしい。
実験
Multusを用いてルーティングソフトウェア (FRR) が動作するPodにNICを追加し、そのNICを経由してKubernetesクラスタの外からのパケットをルーティング・フォワーディングするという実験を行う。本当は色々なNetwork FunctionをPodとして追加したりするともっと面白そう。
環境と構成
下図の通り、macOS上のminikubeにFRRとMultusをデプロイする。
- macOS 10.15.7
- VirtualBox 6.1.16
- minikube v1.17.1
- Kubernetes v1.20.2
- CNI 0.3.1
- CNI plugin v0.8.5
- Multus v3.6
- FRRouting (frr) 7.7-dev_git
1.基盤環境構築
macOSに2つのbridgeを用意する。以下のApple公式リンクに従いシステム環境設定からbridgeを作成する。
Macで仮想ネットワークインターフェイスのブリッジを設定する - Apple サポート
実際にやってみたところ、bridge作成後、適当なIPアドレスをアサインするまでは ipconfig
で認識されなかった。また、作成時に指定したブリッジ名は ipconfig
上では適用されていなかった。(既にbridge0が存在していたことから、自動的に連番になるものと推測。)
## macOS $ ifconfig bridge1 bridge1: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 options=63<RXCSUM,TXCSUM,TSO4,TSO6> ether f2:18:98:45:d8:01 Configuration: id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 ipfilter disabled flags 0x0 Address cache: nd6 options=201<PERFORMNUD,DAD> media: <unknown type> status: inactive $ ifconfig bridge2 bridge2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 options=63<RXCSUM,TXCSUM,TSO4,TSO6> ether f2:18:98:45:d8:02 Configuration: id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 ipfilter disabled flags 0x0 Address cache: nd6 options=201<PERFORMNUD,DAD> media: <unknown type> status: inactive
続いてminikubeを用意する。今回はVirtualBoxのVMで扱いたいため --driver='virtualbox'
を指定。またCNIプラグインとしてciliumを採用する。
## macOS $ minikube start -p minikube-vbox --driver='virtualbox' --cni='cilium' \ --container-runtime=containerd --kubernetes-version='stable'
kubectlの接続を確認したら一度minikube VMを落とす。minikube VMに作成したbridge 2つをNICとして設定したいが、起動時にそれをするオプションはないためVM完成後にVirtualBoxのGUIから実施する。
$ kubectl version Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.2", GitCommit:"faecb196815e248d3ecfb03c680a4507229c2a56", GitTreeState:"clean", BuildDate:"2021-01-13T13:28:09Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.2", GitCommit:"faecb196815e248d3ecfb03c680a4507229c2a56", GitTreeState:"clean", BuildDate:"2021-01-13T13:20:00Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} $ minikube -p minikube-vbox stop
minikube VMにsshしNICを確認する。NICのプロミスキャスモードを有効化し、NICのMACアドレスがVirtualBox GUIで表示されていたMACアドレスと一致していることを確認する。
## macOS $ minikube -p minikube-vbox start $ minikube -p minikube-vbox ssh
## minikube VM $ sudo ip link set dev eth2 promisc on $ sudo ip link set dev eth3 promisc on $ ip addr show eth2 4: eth2: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:b8:f0:85 brd ff:ff:ff:ff:ff:ff $ ip addr show eth3 5: eth3: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:13:a1:e9 brd ff:ff:ff:ff:ff:ff
2.Multusのデプロイ
Multusのquickstartに従う。MultusのPodとCRD (NetworkAttachmentDefinition) がデプロイされている。
## macOS $ git clone https://github.com/intel/multus-cni.git && cd multus-cni $ cat ./images/multus-daemonset.yml | kubectl apply -f - $ kubectl get pods -A -l app=multus NAMESPACE NAME READY STATUS RESTARTS AGE kube-system kube-multus-ds-amd64-dpr7z 1/1 Running 0 19s $ kubectl get crd network-attachment-definitions.k8s.cni.cncf.io NAME CREATED AT network-attachment-definitions.k8s.cni.cncf.io 2021-02-06T07:45:31Z
Nodeであるminikube VMにはMultus用の設定が保存されている。
## minikube VM $ sudo jq . /etc/cni/net.d/00-multus.conf { "cniVersion": "0.3.1", "name": "multus-cni-network", "type": "multus", "kubeconfig": "/etc/cni/net.d/multus.d/multus.kubeconfig", "delegates": [ { "cniVersion": "0.3.1", "name": "cilium", "type": "cilium-cni", "enable-debug": false } ] }
MultusがデプロイされるとNodeの /opt/cni/bin
ディレクトリにプラグインのバイナリが配置される。今回IPアドレスを静的に設定したいと考えたが、使用したい staticより同バージョンのバイナリをダウンロードして直接配置することにした。
## minikube VM $ ls /opt/cni/bin bandwidth bridge cilium-cni cnitool dhcp firewall flannel host-local ipvlan loopback macvlan multus portmap ptp tuning vlan $ ls /opt/cni/bin/static ls: cannot access '/opt/cni/bin/static': No such file or directory
## macOS $ mkdir tmp $ wget -P ./tmp https://github.com/containernetworking/plugins/releases/download/v0.8.5/cni-plugins-linux-amd64-v0.8.5.tgz $ cd tmp && tar zxvf cni-plugins-linux-amd64-v0.8.5.tgz # minikube VMにバイナリを転送するためmountする (他にもっと良い方法あるかも) $ minikube -p minikube-vbox mount ./:/mnt ## minikube VM $ sudo cp /mnt/static /opt/cni/bin/ $ /opt/cni/bin/static version CNI static plugin v0.8.5 ## macOS # minikube mountを停止しておく
3. Kubernetesリソースのデプロイ
今回はmacvlanプラグインを使用してFRRのPodにL2のネットワークを提供する。
下記のマニフェストにより2つのNetworkAttachmentDefinitionリソースをデプロイする。
- ホスト (ここではminikube VM) に接続したNICを指定するために
type: macvlan
でmode: bridge
指定 - NICの指定は
master
で行う promiscMode: true
を指定- IPアドレスのアサインはPod側のannotationで任意に指定することにしたため、
ipam
設定にてtype: static
を指定
## macOS $ cat <<EOF | kubectl apply -f - apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata: name: bridge-conf-1 spec: config: '{ "cniVersion": "0.3.1", "type": "macvlan", "master": "eth2", "mode": "bridge", "promiscMode": true, "ipam": { "type": "static" } }' --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata: name: bridge-conf-2 spec: config: '{ "cniVersion": "0.3.1", "type": "macvlan", "master": "eth3", "mode": "bridge", "promiscMode": true, "ipam": { "type": "static" } }' EOF $ kubectl get network-attachment-definition NAME AGE bridge-conf-1 15s bridge-conf-2 15s
FRRのPodをデプロイする。FRRはZebraを有効化するために securityContext
にて privileged: true
を指定する必要がある。
(今回は実験用なのでこれでよいが、実際にはNodeを守るための手段を講じる必要がある。特にKubernetesが管理しないNICが追加される今回のようなPodではなおさら必要性が増すと考える。)
Podのannotationでは上記で作成したNetworkAttachmentDefinitionリソース (bridge-conf-1
, bridge-conf-2
) とそのbridgeで使用するIPアドレスをそれぞれ指定している。
## macOS $ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: frr annotations: k8s.v1.cni.cncf.io/networks: | [ { "name": "bridge-conf-1", "ips": ["10.0.10.1/24", "2001:db8:10::1/64"] }, { "name": "bridge-conf-2", "ips": ["10.0.20.1/24", "2001:db8:20::1/64"] } ] spec: containers: - name: frr image: frrouting/frr:latest securityContext: privileged: true EOF $ kubectl get pod frr NAME READY STATUS RESTARTS AGE frr 1/1 Running 0 80s
FRR Podでネットワークを確認すると net1
, net2
としてインターフェースが作成されていることが分かる。なお eth0
はKubernetesネットワークに接続するための通常のNICである。
## macOS $ kubectl exec frr -- ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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 inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0.0.0 brd 0.0.0.0 3: net1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 76:97:54:32:af:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.0.10.1/24 brd 10.0.10.255 scope global net1 valid_lft forever preferred_lft forever inet6 2001:db8:10::1/64 scope global valid_lft forever preferred_lft forever inet6 fe80::7497:54ff:fe32:af03/64 scope link valid_lft forever preferred_lft forever 4: net2@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 7a:e6:74:b5:3f:8f brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.0.20.1/24 brd 10.0.20.255 scope global net2 valid_lft forever preferred_lft forever inet6 2001:db8:20::1/64 scope global valid_lft forever preferred_lft forever inet6 fe80::78e6:74ff:feb5:3f8f/64 scope link valid_lft forever preferred_lft forever 43: eth0@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 4a:c4:21:45:63:91 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.0.0.24/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::48c4:21ff:fe45:6391/64 scope link valid_lft forever preferred_lft forever
4.疎通試験用VMの用意
bridgeに接続された適当なVMを2つ用意する。
(本当はコンテナで実施したかったが、docker for macでは host
タイプのネットワークを1つしか作成できないらしく断念)
## macOS $ cat Vagrantfile Vagrant.configure("2") do |config| config.vm.hostname = "vm1" config.vm.box = "ubuntu/xenial64" config.vm.network "public_network", bridge: "bridge1", ip: "10.0.10.10" end $ vagrant up $ vagrant ssh ## VM1 $ ip addr show (snip) 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:da:b2:d8 brd ff:ff:ff:ff:ff:ff inet 10.0.10.10/24 brd 10.0.10.255 scope global enp0s8 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:feda:b2d8/64 scope link valid_lft forever preferred_lft forever # この時点でFRRコンテナと通信が可能 $ ping 10.0.10.1 -c 3 PING 10.0.10.1 (10.0.10.1) 56(84) bytes of data. 64 bytes from 10.0.10.1: icmp_seq=1 ttl=64 time=1.26 ms 64 bytes from 10.0.10.1: icmp_seq=2 ttl=64 time=0.263 ms 64 bytes from 10.0.10.1: icmp_seq=3 ttl=64 time=0.279 ms --- 10.0.10.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2001ms rtt min/avg/max/mdev = 0.263/0.601/1.262/0.467 ms # せっかくなのでIPv6アドレスの設定も $ sudo ip address add 2001:db8:10::10/64 dev enp0s8 # 疎通試験のためにstatic routeを追加 $ sudo ip route add 10.0.20.0/24 via 10.0.10.1 $ sudo ip route add 2001:db8:20::/64 via 2001:db8:10::1
VM2も同様に設定する。
## macOS $ cat Vagrantfile Vagrant.configure("2") do |config| config.vm.hostname = "vm2" config.vm.box = "ubuntu/xenial64" config.vm.network "public_network", bridge: "bridge2", ip: "10.0.20.20" end $ vagrant up $ vagrant ssh ## VM2 $ sudo ip address add 2001:db8:20::20/64 dev enp0s8 $ sudo ip route add 10.0.10.0/24 via 10.0.20.1 $ sudo ip route add 2001:db8:10::/64 via 2001:db8:20::1
5.疎通試験
FRRを設定する。
## macOS $ kubectl exec -it frr bash ## FRR Pod $ vtysh frr# conf t frr(config)# interface net1 frr(config-if)# ip address 10.0.10.1/24 frr(config-if)# ipv6 address 2001:db8:10::1/64 frr(config-if)# no shutdown frr(config-if)# exit frr(config)# interface net2 frr(config-if)# ip address 10.0.20.1/24 frr(config-if)# ipv6 address 2001:db8:20::1/64 frr(config-if)# no shutdown frr(config-if)# exit frr(config)# ip forwarding frr(config)# ipv6 forwarding frr(config)# end
疎通確認を行う。想定通りVM間で通信できていることが分かる。
## VM1 $ ping 10.0.20.20 -c 3 PING 10.0.20.20 (10.0.20.20) 56(84) bytes of data. 64 bytes from 10.0.20.20: icmp_seq=1 ttl=63 time=0.536 ms 64 bytes from 10.0.20.20: icmp_seq=2 ttl=63 time=1.34 ms 64 bytes from 10.0.20.20: icmp_seq=3 ttl=63 time=1.09 ms --- 10.0.20.20 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 0.536/0.989/1.342/0.337 ms $ ping6 2001:db8:20::20 -c 3 PING 2001:db8:20::20(2001:db8:20::20) 56 data bytes 64 bytes from 2001:db8:20::20: icmp_seq=1 ttl=63 time=0.531 ms 64 bytes from 2001:db8:20::20: icmp_seq=2 ttl=63 time=0.609 ms 64 bytes from 2001:db8:20::20: icmp_seq=3 ttl=63 time=1.24 ms --- 2001:db8:20::20 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2000ms rtt min/avg/max/mdev = 0.531/0.794/1.242/0.318 ms
ちなみに仮想環境なのであまり意味はないが、通信帯域としては300Mbps程度であった。
## VM1 $ sudo iperf3 -c 10.0.20.20 (snip) - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bandwidth Retr [ 4] 0.00-10.00 sec 365 MBytes 306 Mbits/sec 258 sender [ 4] 0.00-10.00 sec 363 MBytes 304 Mbits/sec receiver
所感
上記の通りKubernetesで管理されているコンテナでクラスタ外からのパケットルーティング・フォワーディングを簡単に行うことができた。Webアプリの動作など通常のKubernetesの使い方からするとかなりイレギュラーなことをやっている気持ちになった。今後コンテナベースのNetwork Function (CNF) が流行るのかどうか分からないが、一つ興味深いOSSだった。
一つのユースケースとしておうちKubernetesでご家庭のネットワーク機能を実現できたら面白そう。
一方でセキュリティについては意識する必要があると思う。Multusによって生えたNICはKubernetes側に感知されないため、Kuberneteが提供するセキュリティ機能を利用することができないため、Firewallなどネットワークプレイヤーでのセキュリティを十分に担保する必要があると感じた。その辺りもVirtual Function的にPodで提供して対応するべきか。