MultusによるKubernetesクラスタ外からのパケット転送実験

Multus

最近になってMultusというKubernetes CNIの一つを知った。Kubernetes環境ではPodは通常1つのNICのみを持つが、Multusを使うことでPodに複数のNICを持たせることが可能になる。

上記情報源より簡単にその特徴を挙げる。詳細な説明は各記事を参照のこと。(2つの日本語記事はとても分かりやすくまとめられていて助かります。)

  • intel社のOSSであり、コンテナ環境でNFVを実現するための課題 (複数NIC、通信の分離や高速化など) を解決する一手法
  • Kubernetesで管理されるコンテナがホストサーバーのNICを介して通信高速化機能 (DPDKやSR-IOVなど) を利用することができる
  • Multus自体は「Delegating CNIプラグイン」であり、別途CNIプラグインの存在を前提とし、それらに対してネットワーク設定をdelegateする形でNICを設定する
  • Multusが作成するNICKubernetes側では感知されない
  • 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をデプロイする。

f:id:nstgt7:20210206183929p:plain

  • 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 サポート

f:id:nstgt7:20210206153537p:plain

実際にやってみたところ、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を用意する。今回はVirtualBoxVMで扱いたいため --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完成後にVirtualBoxGUIから実施する。

$ 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 VMsshNICを確認する。NICのプロミスキャスモードを有効化し、NICMACアドレス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: macvlanmode: 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 としてインターフェースが作成されていることが分かる。なお eth0Kubernetesネットワークに接続するための通常の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間で通信できていることが分かる。

f:id:nstgt7:20210206184026p:plain

## 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によって生えたNICKubernetes側に感知されないため、Kuberneteが提供するセキュリティ機能を利用することができないため、Firewallなどネットワークプレイヤーでのセキュリティを十分に担保する必要があると感じた。その辺りもVirtual Function的にPodで提供して対応するべきか。