Multusで遊ぶ

Red Hatでコンサルタントをしている織です。本記事では、KubernetesのPodに複数のNICを接続するための、MultusというCNIプラグインについてご紹介します。

Multusとは

MultusはKubernetesのCNI (Container Network Interface) プラグインのひとつです。典型的なKubernetesの構成では、PodにはひとつのNICしかアサインされませんが、Multusを使うとPodに追加のNICを生やして、複数のネットワークに接続することができるようになります。内部的には、Multusは複数のCNIプラグインを同時に稼働させるための「メタCNIプラグイン」として稼働します。

f:id:orimanabu:20200117131207p:plain
Multus (https://github.com/intel/multus-cni より)

Multusは、Kubernetesの Network Plumbing Working Group で議論されている Kubernetes Network Custom Resource Definition De-facto Standard という標準にしたがっており、その参照実装として開発されてきました。

準備

CentOS7とKubernetes v1.17を使って、動作検証環境を準備します。

まず、NICを2つ(eth0, eth1)持つCentOS7の仮想マシンを3台作成し、kubeadm を使って、masterノード1台 + workerノード2台のクラスタを作成します。

Multusでは複数のCNIプラグインを併用しますが、そのうちのひとつをDefault(もしくはMaster)プラグインと呼びます。PodからAPIへの通信やLiveness/Readiness Probe等、従来のPod接続で行っていた通信は、基本的にDefaultプラグインを使います。

以下の操作では、DefaultのプラグインとしてFlannelを使います。その準備として、まず(Multusではない)普通のCNIプラグインとしてFlannelをインストールします。

kube-master $ kubectl create -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

デフォルトゲートウェイが設定されているNIC(手元の環境ではeth0)でFlannelを使うように設定されます。 他方のNIC(手元の環境ではeth1)を、Podを接続するもうひとつのネットワーク接続に使用します。

次に、Multusに必要な諸々を入れます[1]。 Multusでは、接続するネットワークをNetworkAttachmentDefinition(もしくはnet-attach-def)というCustom Resourceとして表現します。下記コマンドで、その定義(CRD, Custom Resource Definition)や、MultusのCNIデーモンをデプロイするDaemonSet等をインストールします。

kube-master $ kubectl create -f https://raw.githubusercontent.com/intel/multus-cni/master/images/multus-daemonset.yml

以上で準備は完了です。インストールが完了すると、下記のような感じになります。

kube-master $ kubectl get pod --all-namespaces -o wide
NAMESPACE     NAME                                  READY   STATUS    RESTARTS   AGE    IP                NODE          NOMINATED NODE   READINESS GATES
kube-system   coredns-6955765f44-bmrln              1/1     Running   0          4d2h   10.244.1.2        kube-node-2   <none>           <none>
kube-system   coredns-6955765f44-qh567              1/1     Running   0          4d2h   10.244.0.2        kube-master   <none>           <none>
kube-system   etcd-kube-master                      1/1     Running   1          4d2h   192.168.122.125   kube-master   <none>           <none>
kube-system   kube-apiserver-kube-master            1/1     Running   1          4d2h   192.168.122.125   kube-master   <none>           <none>
kube-system   kube-controller-manager-kube-master   1/1     Running   1          4d2h   192.168.122.125   kube-master   <none>           <none>
kube-system   kube-flannel-ds-amd64-fv8s4           1/1     Running   0          4d     192.168.122.172   kube-node-1   <none>           <none>
kube-system   kube-flannel-ds-amd64-nx85b           1/1     Running   0          4d     192.168.122.143   kube-node-2   <none>           <none>
kube-system   kube-flannel-ds-amd64-zjt4d           1/1     Running   0          4d     192.168.122.125   kube-master   <none>           <none>
kube-system   kube-multus-ds-amd64-4fhfh            1/1     Running   0          4d     192.168.122.172   kube-node-1   <none>           <none>
kube-system   kube-multus-ds-amd64-4n2nq            1/1     Running   0          4d     192.168.122.125   kube-master   <none>           <none>
kube-system   kube-multus-ds-amd64-gkdn5            1/1     Running   0          4d     192.168.122.143   kube-node-2   <none>           <none>
kube-system   kube-proxy-g7wwf                      1/1     Running   1          4d2h   192.168.122.172   kube-node-1   <none>           <none>
kube-system   kube-proxy-ls8x5                      1/1     Running   1          4d2h   192.168.122.125   kube-master   <none>           <none>
kube-system   kube-proxy-n9w4g                      1/1     Running   1          4d2h   192.168.122.143   kube-node-2   <none>           <none>
kube-system   kube-scheduler-kube-master            1/1     Running   1          4d2h   192.168.122.125   kube-master   <none>           <none>

この手順で作成されたCNIプラグインとしてのMultusの設定は、下記のようになります。

kube-master $ jq . /etc/cni/net.d/00-multus.conf
{
  "cniVersion": "0.3.1",
  "name": "multus-cni-network",
  "type": "multus",
  "logLevel": "debug",
  "logFile": "/tmp/multus.log",
  "binDir": "/opt/multus/bin",
  "kubeconfig": "/etc/cni/net.d/multus.d/multus.kubeconfig",
  "delegates": [
    {
      "name": "cbr0",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  ]
}

Multusインストール前のFlannelのCNI設定が、delegates 以下にそのまま入っている形になっています。

普通のPodのデプロイ

まずは従来どおり、NICをひとつだけ持つPodをデプロイします。この場合、DefaultプラグインであるFlannelのネットワークに接続します。

kube-master $ cat unmultus-pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
  generateName: unmultus-
  labels:
    app: unmultus
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true

のようなmanifestを作ってデプロイします。

kube-master $ kubectl create -f unmultus-pod.yaml
pod/unmultus-7wpr6 created
$ kubectl get pod -l app=unmultus
NAME             READY   STATUS    RESTARTS   AGE
unmultus-7wpr6   1/1     Running   0          16s

いつもどおり、eth0 として veth のインターフェースがあることがわかります。

kube-master $ kubectl exec -it unmultus-7wpr6 -- ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if54: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether fe:12:e9:69:80:a5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.2.47/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::fc12:e9ff:fe69:80a5/64 scope link
       valid_lft forever preferred_lft forever

vlanで追加ネットワークに接続

Multusを使って、コンテナホストのeth1にVLANサブインターフェースを作成し、それをPodの2つ目のNICとしてアサインします。

NetworkAttachmentDefinitionの定義

まず、PodをVLANで接続する追加ネットワークをNetworkAttachmentDefinitionのCustom Resourceとして定義します。ここでは vlan-conf という名前をつけています (metadata.name: vlan-conf)。

kube-master $ cat vlan-conf.yaml
---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: vlan-conf
spec:
  config: '{
            "cniVersion": "0.3.1",
            "plugins": [
                {
                    "type": "vlan",
                    "master": "eth1",
                    "vlanid": 100,
                    "capabilities": { "ips": true },
                    "ipam": {
                        "type": "static"
                    }
                }, {
                    "capabilities": { "mac": true },
                    "type": "tuning"
                } ]
        }'

今回作成したKubernetes環境は、DefaultプラグインのFlannelが作るネットワーク通信に、コンテナホストの eth0 を使っています。 上記のNetworkAttachmentDefinitionは、Podが接続する(Flannelとは別の)ネットワークとして、コンテナホスト上の eth1 が接続するVLAN ID 100のtagged VLANを使うことを意味します。

kube-master $ kubectl create -f vlan-conf.yaml
networkattachmentdefinition.k8s.cni.cncf.io/vlan-conf created
$ kubectl get net-attach-def vlan-conf
NAME        AGE
vlan-conf   29s

のように vlan-conf のNetworkAttachmentDefinitionオブジェクトを作成します。

Podの作成

次のようなmanifestでPodをデプロイします。

kube-master $ cat vlan-pod1.yaml
---
apiVersion: v1
kind: Pod
metadata:
#  generateName: vlan-
  name: vlan-172.16.1.201
  labels:
    app: multus-vlan
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
            { "name": "vlan-conf",
              "ips": [ "172.16.1.201/24" ]}
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true

metadata.annotations.k8s.v1.cni.cncf.io/networks で先ほど作成したnet-attach-defの名前 vlan-conf を指定しています。

kube-master $ kubectl create -f vlan-pod1.yaml
pod/vlan-172.16.1.201 created

同様にして、うまく異なるworkerノードにスケジューリングされることを祈りつつ[2]、 172.16.1.202 のIPアドレスをアサインしたPodをデプロイします。

kube-master $ kubectl get pod -l app=multus-vlan -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP            NODE          NOMINATED NODE   READINESS GATES
vlan-172.16.1.201   1/1     Running   0          3m44s   10.244.2.48   kube-node-1   <none>           <none>
vlan-172.16.1.202   1/1     Running   0          2m9s    10.244.1.24   kube-node-2   <none>           <none>

Pod vlan-172.16.1.201 のインターフェースを確認します。

kube-master $ kubectl exec -it vlan-172.16.1.201 -- ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if61: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether b6:1e:bd:66:b9:cd brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.2.52/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::b41e:bdff:fe66:b9cd/64 scope link
       valid_lft forever preferred_lft forever
4: net1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 52:54:00:5d:8f:e5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    vlan protocol 802.1Q id 100 <REORDER_HDR> numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 172.16.1.201/24 brd 172.16.1.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe5d:8fe5/64 scope link
       valid_lft forever preferred_lft forever

eth0 はDefaultプラグインであるFlannelにつながるインターフェースです。それに加えて、net1 としてVLAN ID 100のvlanインターフェースが追加され、manifestで指定したIPアドレス(172.16.2.201)が付与されていることがわかります。

Pod vlan-172.16.1.202net1 も念の為確認しておきます。

kube-master $ kubectl exec -it vlan-172.16.1.202 -- ip -d addr show dev net1
4: net1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 52:54:00:39:4d:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    vlan protocol 802.1Q id 100 <REORDER_HDR> numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 172.16.1.202/24 brd 172.16.1.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe39:4dc5/64 scope link
       valid_lft forever preferred_lft forever

同じくVLAN ID 100のvlanインターフェースとして、172.16.1.202 がアサインされています。

このPodがスケジュールされたworkerノード kube-node-1 のインターフェースは下記のようになっています。

kube-node-1 $ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:d4:9d:eb brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:5d:8f:e5 brd ff:ff:ff:ff:ff:ff
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether 8e:f7:5f:9f:84:ff brd ff:ff:ff:ff:ff:ff
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 8a:b9:22:00:44:9b brd ff:ff:ff:ff:ff:ff
55: veth0ce3de47@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default
    link/ether fa:64:45:b0:ea:38 brd ff:ff:ff:ff:ff:ff link-netnsid 0

indexが3なのは eth1 なので、Pod内の net1 の親インターフェースはホストの eth1 であることがわかります。 また、今回は priviledged: true としてPodを起動しているので、Pod内で下記のようにして、ホスト上の物理インターフェースを確認することもできます。

kube-master $ kubectl exec -it vlan-172.16.1.201 -- cat /proc/net/vlan/net1
net1  VID: 100   REORDER_HDR: 1  dev->priv_flags: 1021
         total frames received           15
          total bytes received          976
      Broadcast/Multicast Rcvd           15

      total frames transmitted           13
       total bytes transmitted         1046
Device: eth1
INGRESS priority mappings: 0:0  1:0  2:0  3:0  4:0  5:0  6:0 7:0
 EGRESS priority mappings:

最後に、2つのPodの net1 同士で通信できることを確認します。

kube-master $ kubectl exec -it vlan-172.16.1.201 -- ping -c 3 172.16.1.202
PING 172.16.1.202 (172.16.1.202) 56(84) bytes of data.
64 bytes from 172.16.1.202: icmp_seq=1 ttl=64 time=0.995 ms
64 bytes from 172.16.1.202: icmp_seq=2 ttl=64 time=0.901 ms
64 bytes from 172.16.1.202: icmp_seq=3 ttl=64 time=0.667 ms

--- 172.16.1.202 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 0.667/0.854/0.995/0.139 ms

また、下記のようにmasterノードのeth1にVLAN ID 100のVLANサブインターフェースを作成すると、masterノードとPodの net1 の間で通信することができます。

kube-master $ sudo ip link add link eth1 name eth1.100 type vlan id 100
kube-master $ sudo ip addr add 172.16.1.10/24 dev eth1.100
kube-master $ sudo ip link set up eth1.100
kube-master $ ping -c 3 172.16.1.201
PING 172.16.1.201 (172.16.1.201) 56(84) bytes of data.
64 bytes from 172.16.1.201: icmp_seq=1 ttl=64 time=1.10 ms
64 bytes from 172.16.1.201: icmp_seq=2 ttl=64 time=0.542 ms
64 bytes from 172.16.1.201: icmp_seq=3 ttl=64 time=0.566 ms

--- 172.16.1.201 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2046ms
rtt min/avg/max/mdev = 0.542/0.739/1.109/0.261 ms

macvlanで追加ネットワークに接続

macvlanとは

macvlanは、物理インターフェースに対して、異なるMACアドレスとIPアドレスを持つ複数のサブインターフェースを作成する仕組みです。 Linuxのbridgeインターフェースと似ている部分が多いのですが、

  • bridgeはMAC Learning、STP等、いわゆるL2スイッチと同じ動きをする。macvlanでは、すべてのサブインターフェースのMACアドレスがわかるので、MAC LeariningやSTPは必要ない
  • macvlanサブインターフェースと親の物理インターフェースは直接は通信できない

という違いがあります。またvlanサブインターフェースとは

  • vlanサブインターフェースは親インターフェースと同じMACアドレスを持ち、VLANタグを使って異なるL2ドメインに接続するのに対して、macvlanサブインターフェースは親インターフェースとは異なるMACアドレスを持ち、外部ネットワークに直接接続する

という違いがあります。

macvlanでは、親インターフェースが複数のMACアドレス宛てのパケットを受信する必要があります。親インターフェースがUnicast filteringの機能 (IFF_UNICAST_FLT) をサポートする場合は、受信を許可する(macvlanサブインターフェースの)MACアドレスを明示的に登録します。Unicast filteringをサポートしない場合は親インターフェースをPromiscuousモードにします。検証で使った環境では親インターフェースとしてvirtio_netを使っており、virtio_netはUnicast filteringをサポートしています(ので、以下の例では親インターフェースはPromiscuousにはなっていません)。

いくつかの動作モードがありますが、ここでは bridge モードを使います。

NetworkAttachmentDefinitionの定義

Podをmacvlanで接続する追加ネットワークをNetworkAttachmentDefinitionのCustom Resourceとして定義します。ここでは macvlan-conf という名前をつけています (metadata.name: macvlan-conf)。

kube-master $ cat macvlan-conf.yaml
---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: macvlan-conf
spec:
  config: '{
            "cniVersion": "0.3.1",
            "plugins": [
                {
                    "type": "macvlan",
                    "capabilities": { "ips": true },
                    "master": "eth1",
                    "mode": "bridge",
                    "ipam": {
                        "type": "static"
                    }
                }, {
                    "capabilities": { "mac": true },
                    "type": "tuning"
                } ]
        }'

上記のNetworkAttachmentDefinitionでは、ホストの eth1 を親インターフェースとして、bridge modeのmacvlanインターフェースを作成し、それをPodをアサインします。

kube-master $ kubectl create -f macvlan-conf.yaml
networkattachmentdefinition.k8s.cni.cncf.io/macvlan-conf created
$ kubectl get net-attach-def macvlan-conf
NAME           AGE
macvlan-conf   8s

Podの作成

さらに次のようなmanifestでPodをデプロイします。

kube-master $ cat macvlan-pod1.yaml
---
apiVersion: v1
kind: Pod
metadata:
#  generateName: macvlan-
  name: macvlan-10.1.1.201
  labels:
    app: multus-macvlan
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
            {
              "name": "macvlan-conf",
              "ips": [ "10.1.1.201/24" ]
            }
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true

metadata.annotations.k8s.v1.cni.cncf.io/networks で、先ほど作成したnet-attach-defの名前 macvlan-conf を指定しています。

kube-master $ kubectl create -f macvlan-pod1.yaml
pod/macvlan-10.1.1.201 created

同様にして、10.1.1.202 のIPアドレスをアサインしたPodをデプロイします。

kube-master $ kubectl create -f macvlan-pod2.yaml
pod/macvlan-10.1.1.202 created

Podが2個デプロイされました。

kube-master $ kubectl get pod -l app=multus-macvlan -o wide
NAME                 READY   STATUS    RESTARTS   AGE     IP            NODE          NOMINATED NODE   READINESS GATES
macvlan-10.1.1.201   1/1     Running   0          2m47s   10.244.2.49   kube-node-1   <none>           <none>
macvlan-10.1.1.202   1/1     Running   0          93s     10.244.1.25   kube-node-2   <none>           <none>

Podのインターフェースを確認すると、bridgeモードのmacvlanインターフェースとして net1 が追加され、IPアドレス 10.1.1.201 が付与されていることがわかります。

kube-master $ kubectl exec -it macvlan-10.1.1.201 -- ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if56: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 9a:6e:b6:d9:e8:a3 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.2.49/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::986e:b6ff:fed9:e8a3/64 scope link
       valid_lft forever preferred_lft forever
4: net1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether aa:2a:7c:df:12:b9 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    macvlan mode bridge numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.1.1.201/24 brd 10.1.1.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::a82a:7cff:fedf:12b9/64 scope link
       valid_lft forever preferred_lft forever

デプロイしたPod間の疎通を確認します。

kube-master $ kubectl exec -it macvlan-10.1.1.201 -- ping -c 3 10.1.1.202
PING 10.1.1.202 (10.1.1.202) 56(84) bytes of data.
64 bytes from 10.1.1.202: icmp_seq=1 ttl=64 time=0.716 ms
64 bytes from 10.1.1.202: icmp_seq=2 ttl=64 time=0.858 ms
64 bytes from 10.1.1.202: icmp_seq=3 ttl=64 time=0.686 ms

--- 10.1.1.202 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2008ms
rtt min/avg/max/mdev = 0.686/0.753/0.858/0.078 ms

デプロイしたPodとmasterノードとの疎通を確認します (masterノードのeth1に 10.1.1.1 をアサインしています)。

kube-master $ kubectl exec -it macvlan-10.1.1.201 -- ping -c 3 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=6.25 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.638 ms
64 bytes from 10.1.1.1: icmp_seq=3 ttl=64 time=0.754 ms

--- 10.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 0.638/2.548/6.252/2.619 ms

ipvlanで追加ネットワークに接続

ipvlanとは

ipvlanは、macvlanととてもよく似ています。macvlanとの違いは、サブインターフェースごとに固有のMACアドレスが付与されず、サブインターフェースは親インターフェースと同じMACアドレスを持つことです。ipvlanには、親インターフェースとMACアドレスを共有することに起因する、いくつかの考慮点があります。

  • サブインターフェースのIPアドレスをDHCPで取得するには工夫が必要。具体的には、ipvlanサブインターフェースを割り当てる仮想マシンもしくはコンテナからのリクエストに対して固有のClient IDを指定し、DHCPサーバ側ではMACアドレスではなくClient IDに対してIPアドレスを割り当てるようにする。
  • サブインターフェースのMACアドレスが共通であるため、MACアドレスからEUI-64フォーマットでインターフェースIDを生成するSLAACによるIPv6の自動設定は使えない

一方で、いくつかのユースケースではmacvlanよりもipvlanの方が適しているとカーネルのドキュメントに記載されています。 例として挙げられているのは下記です。

  • 親インターフェースの対向装置のポートが、ひとつのMACアドレスしか許可しないポリシーになっている
  • 親インターフェースで処理できるMACアドレスに上限がある、Promiscuousモードにできない、(macvlanの)パフォーマンス/スケーラビリティに懸念がある
  • サブインターフェースを割り当てるコンテナや仮想マシンが信用できない (MACアドレスを書き換えたりしてほしくない)

カーネルの準備

CentOS7のカーネル(確認したバージョンは3.10.0-957.27.2.el7)は、ipvlanが有効になっていません。以下では、ELRepoのkernel-ml (mainline stableバージョンのupstreamカーネルをCentOS7用にコンパイルしたもの) を使ってipvlanの動作を確認しました。実際に使ったバージョンは5.4.10-1.el7.elrepoです。

NetworkAttachmentDefinitionの定義

Podをipvlanで接続する追加ネットワークをNetworkAttachmentDefinitionのCustom Resourceとして定義します。ここでは ipvlan-conf という名前をつけています (metadata.name: ipvlan-conf)。

kube-master $ cat ipvlan-conf.yaml
---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-conf
spec:
  config: '{
            "cniVersion": "0.3.1",
            "plugins": [
                {
                    "type": "ipvlan",
                    "capabilities": { "ips": true },
                    "master": "eth1",
                    "ipam": {
                        "type": "static"
                    }
                }, {
                    "capabilities": { "mac": true },
                    "type": "tuning"
                } ]
        }'

上記のNetworkAttachmentDefinitionでは、ホストの eth1 を親インターフェースとして、ipvlanインターフェースを作成し、それをPodをアサインします。

kube-master $ kubectl create -f ipvlan-conf.yaml
networkattachmentdefinition.k8s.cni.cncf.io/ipvlan-conf created
$ kubectl get net-attach-def ipvlan-conf
NAME          AGE
ipvlan-conf   8s

Podの作成

次のようなmanifestでPodをデプロイします。

kube-master $ cat ipvlan-pod1.yaml
---
apiVersion: v1
kind: Pod
metadata:
#  generateName: ipvlan-
  name: ipvlan-10.1.1.211
  labels:
    app: multus-ipvlan
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
            { "name": "ipvlan-conf",
              "ips": [ "10.1.1.211/24" ] }
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true

metadata.annotations.k8s.v1.cni.cncf.io/networks で、先ほど作成したnet-attach-defの名前 ipvlan-conf を指定しています。

kube-master $ kubectl create -f ipvlan-pod1.yaml
pod/ipvlan-10.1.1.211 created

同様にして、10.1.1.202 のIPアドレスをアサインしたPodをデプロイします。

kube-master $ kubectl create -f ipvlan-pod2.yaml
pod/ipvlan-10.1.1.212 created

Podが2個デプロイされました。

kube-master $ kubectl get pod -l app=multus-ipvlan -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP            NODE          NOMINATED NODE   READINESS GATES
ipvlan-10.1.1.211   1/1     Running   0          73s   10.244.2.50   kube-node-1   <none>           <none>
ipvlan-10.1.1.212   1/1     Running   0          17s   10.244.1.26   kube-node-2   <none>           <none>

Podのインターフェースを確認すると、l2モードのipvlanインターフェースとして net1 が追加され、IPアドレス 10.1.1.211 が付与されていることがわかります。

kube-master $ kubectl exec -it ipvlan-10.1.1.211 -- ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if57: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 82:a6:ba:07:63:08 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.2.50/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::80a6:baff:fe07:6308/64 scope link
       valid_lft forever preferred_lft forever
4: net1@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
    link/ether 52:54:00:5d:8f:e5 brd ff:ff:ff:ff:ff:ff promiscuity 0
    ipvlan  mode l2 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.1.1.211/24 brd 10.1.1.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::5254:0:15d:8fe5/64 scope link
       valid_lft forever preferred_lft forever

デプロイしたPod間の疎通を確認します。

kube-master $ kubectl exec -it ipvlan-10.1.1.211 -- ping -c 3 10.1.1.212
PING 10.1.1.212 (10.1.1.212) 56(84) bytes of data.
64 bytes from 10.1.1.212: icmp_seq=1 ttl=64 time=0.703 ms
64 bytes from 10.1.1.212: icmp_seq=2 ttl=64 time=0.781 ms
64 bytes from 10.1.1.212: icmp_seq=3 ttl=64 time=1.16 ms

--- 10.1.1.212 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2078ms
rtt min/avg/max/mdev = 0.703/0.881/1.161/0.202 ms

デプロイしたPodとmasterノードとの疎通を確認します。

kube-master $ kubectl exec -it ipvlan-10.1.1.211 -- ping -c 3 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.708 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.724 ms
64 bytes from 10.1.1.1: icmp_seq=3 ttl=64 time=0.341 ms

--- 10.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2073ms
rtt min/avg/max/mdev = 0.341/0.591/0.724/0.176 ms

bridgeで追加ネットワークに接続

NetworkAttachmentDefinitionの定義

PodをLinux Bridge経由で接続する追加ネットワークをNetworkAttachmentDefinitionのCustom Resourceとして定義します。ここでは bridge-conf という名前をつけています (metadata.name: bridge-conf)。

kube-master $ cat bridge-conf.yaml
---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: bridge-conf
spec:
  config: '{
            "cniVersion": "0.3.1",
            "plugins": [
                {
                    "type": "bridge",
                    "name": "br-multus",
                    "bridge": "br-multus0",
                    "capabilities": { "ips": true },
                    "ipam": {
                        "type": "static"
                    }
                } ]
        }'

上記のNetworkAttachmentDefinitionでは、ホストに br-multus0 という名前のブリッジを作成し、そこにPodのvethを接続します。

kube-master $ kubectl create -f bridge-conf.yaml
networkattachmentdefinition.k8s.cni.cncf.io/bridge-conf created
$ kubectl get net-attach-def bridge-conf
NAME          AGE
bridge-conf   9s

Podの作成

次のようなmanifestでPodをデプロイします。

kube-master $ cat bridge-pod1.yaml
---
apiVersion: v1
kind: Pod
metadata:
#  generateName: bridge-
  name: bridge-10.1.1.221
  labels:
    app: multus-bridge
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
            {
              "name": "bridge-conf",
              "ips": [ "10.1.1.221/24" ]
            }
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true

metadata.annotations.k8s.v1.cni.cncf.io/networks で、先ほど作成したnet-attach-defの名前 bridge-conf を指定しています。

kube-master $ kubectl create -f bridge-pod1.yaml
pod/bridge-10.1.1.221 created

同様にして、10.1.1.222 のIPアドレスをアサインしたPodをデプロイします。

kube-master $ kubectl create -f bridge-pod2.yaml
pod/bridge-10.1.1.222 created

Podが2個デプロイされました。

kube-master $ kubectl get pod -l app=multus-bridge -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP            NODE          NOMINATED NODE   READINESS GATES
bridge-10.1.1.221   1/1     Running   0          97s   10.244.2.51   kube-node-1   <none>           <none>
bridge-10.1.1.222   1/1     Running   0          27s   10.244.1.27   kube-node-2   <none>           <none>

Podのインターフェースを確認すると、vethとして net1 が追加され、IPアドレス 10.1.1.221 が付与されていることがわかります。

kube-master $ kubectl exec -it bridge-10.1.1.221 -- ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if63: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether f6:a7:c8:89:40:f6 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.2.54/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::f4a7:c8ff:fe89:40f6/64 scope link
       valid_lft forever preferred_lft forever
5: net1@if65: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 92:a8:48:c2:f0:55 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.1.1.221/24 brd 10.1.1.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::90a8:48ff:fec2:f055/64 scope link
       valid_lft forever preferred_lft forever

Pod bridge-10.1.1.221 がスケジュールされたノード kube-node-1 上で、bridgeインターフェースを確認します。

kube-node-1 $ ip link show type bridge
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 8a:b9:22:00:44:9b brd ff:ff:ff:ff:ff:ff
64: br-multus0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 1e:86:7b:f3:e8:da brd ff:ff:ff:ff:ff:ff

Flannelで使う cni0 に加えて、net-attach-defで指定した br-multus0 というブリッジが作成されています。

kube-node-1 $ brctl show
bridge name     bridge id               STP enabled     interfaces
br-multus0              8000.1e867bf3e8da       no              vethc1d59074
cni0            8000.8ab92200449b       no              veth8911ce6e
                                                        vethdcfe4db8

により、ホスト上で br-multus0 に接続するインターフェースは vethc1d59074 であることがわかります。 さらにこのvethのifindexを確認します (65でした)。

kube-node-1 $ ip -d link show dev vethc1d59074
65: vethc1d59074@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-multus0 state UP mode DEFAULT group default
    link/ether 1e:86:7b:f3:e8:da brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 1
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin off guard off root_block off fastleave off learning on flood on port_id 0x8001 port_no 0x1 designated_port 32769 designated_cost 0 designated_bridge 8000.1e:86:7b:f3:e8:da designated_root 8000.1e:86:7b:f3:e8:da hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

Pod内の net1 のインターフェース情報(下記に再掲)と突き合わせると、確かに上記 vethc1d59074 とPod bridge-10.1.1.221net1 がveth pairになっていることが確認できます。

kube-master $ kubectl exec -it bridge-10.1.1.221 -- ip -d link show dev net1
5: net1@if65: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 92:a8:48:c2:f0:55 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

さて、この状態ではブリッジ br-multus0 のアップリンクが設定されていないため、異なるノード上のPodとは通信できません。 各workerノードで、br-multus0 のアップリンクとして eth1 を設定します。

kube-node-1 $ sudo ip link set eth1 master br-multus0
kube-node-1 $ brctl show
bridge name     bridge id               STP enabled     interfaces
br-multus0              8000.5254005d8fe5       no              eth1
                                                        veth9b1f5832
cni0            8000.8ab92200449b       no              vethdcfe4db8
                                                        vethe5ca8264

こうすることで、デプロイしたPod間で疎通できるようになります。

kube-master $ kubectl exec -it bridge-10.1.1.221 -- ping -c 3 10.1.1.222
PING 10.1.1.222 (10.1.1.222) 56(84) bytes of data.
64 bytes from 10.1.1.222: icmp_seq=1 ttl=64 time=1.52 ms
64 bytes from 10.1.1.222: icmp_seq=2 ttl=64 time=0.824 ms
64 bytes from 10.1.1.222: icmp_seq=3 ttl=64 time=0.860 ms

--- 10.1.1.222 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.824/1.070/1.527/0.324 ms

デプロイしたPodとmasterノードとの疎通を確認します (masterノードのeth1に 10.1.1.1 をアサインしています)。

kube-master $ kubectl exec -it bridge-10.1.1.221 -- ping -c 3 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.601 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.215 ms
64 bytes from 10.1.1.1: icmp_seq=3 ttl=64 time=0.329 ms

--- 10.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2057ms
rtt min/avg/max/mdev = 0.215/0.381/0.601/0.163 ms

IPAM

Podに割り当てるIPアドレスの管理方法もプラグイン形式になっており、現時点では3種類から選べるようになっています。

  • static
  • host-local
  • DHCP

本記事ではstaticを使用し、Podの net1 に付与するIPアドレスは、Pod manifestに直書きしました。 その他の方法については、また別途何か書く...かもしれません。

おまけ

余談ですが、レッドハットのMultus開発チームの人が使っているAnsible Playbookがここにあります。これを使うと、下記のような感じでKVM環境の仮想マシンとしてNICを2個持つ仮想マシンインスタンスをお手軽に構築できます。

  • 仮想マシンを作成する [3]
$ ansible-playbook -i inventory/virthost.inventory -e 'network_type=none-2nics' playbooks/virthost-setup.yml
  • コンテナランタイムをCRI-OにしてKubernetesをインストールする [4]
$ ansible-playbook -i inventory/vms.local.generated -e 'pod_network_type=none-2nics' -e 'container_runtime=crio' playbooks/kube-install.yml

謝辞

本記事を書くに当たり、Red HatでNFVPEをしている林さんにいろいろ教えていただきました。ありがとうございました!

開発している人が身近にいるのは最高の福利厚生ですね。


  1. もしかしたら、Multusセットアップ後にPodをデプロイすると、CNIで使うバイナリのパス関連のエラー(具体的には Failed to create pod sandbox: rpc error: code = Unknown (中略) failed to find plugin "multus" in path [/usr/libexec/cni] というメッセージ)で起動に失敗するかもしれません。その場合、このように /opt/cni/bin/usr/libexec/cni に変えて試してみてください。

  2. vlanのNetworkAttachmentDefinitionを使う場合、VLAN IDごとにひとつVLANサブインターフェースを作成してPodのnetwork namespaceにアサインする、という仕組み上、ひとつのホストに同じvlan net-attach-defを使うPodを複数デプロイできないため

  3. もうすぐ ansible-playbook -i inventory/virthost.inventory -e 'network_type=2nics' playbooks/virthost-setup.yml のように変わる予定です

  4. もうすぐ ansible-playbook -i inventory/vms.local.generated -e 'network_type=2nics' -e 'container_runtime=crio' playbooks/kube-install.yml のように変わる予定です

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。