通常、KubernetesのPodに複数のPodを構成する場合は Multus-CNI の利用が一般的ですが、CRIにcontainerdを利用している場合には、単純に複数のNICを刺すためのもっと簡単な方法がありますよ、というエントリです。
このエントリは、Z Lab Advent Calendar 2022 の七日目の記事です。
TL;DR
config.toml
における max_conf
の設定を変更する。
[plugins."io.containerd.grpc.v1.cri".cni]
max_conf_num = 2
環境
この記事は、以下のソフトウェアを利用してテストしています。
- Kubernetes: 1.24
- containerd: 1.6.8
CRI が CNI をキックする
この設定項目を知ったのは、ふと、CRI が CNI を呼び出す際の仕様はどうなっているのか気になったからでした。
CNI の仕様的には、CNI のバイナリに対して環境変数と標準入力に設定ファイルを渡す だけなのだが、CRIはどのCNIをどういう優先度で決定して呼び出しているのだろうか?
CRIに設定したCNIの設定ファイルディレクトリ(デフォルト: /etc/cni/net.d
) にある設定ファイルのうち、辞書順でソートして1番目の設定ファイルが利用されることは知っていたのだけれども、その仕様はどこに明記されているのやら。
とりあえず、ちょっと探しただけでは見つからなかったので、知っている人がいたら教えてください。
containerd の実装
と言うことで探検隊は真実を知るために、containerd
のジャングルの奥地へと向かったのでした。
// loadFromConfDir detects network config files from the
// configured cni config directory and load them. max is
// the maximum network config to load (max i<= 0 means no limit).
func loadFromConfDir(c *libcni, max int) error {
files, err := cnilibrary.ConfFiles(c.pluginConfDir, []string{".conf", ".conflist", ".json"})
switch {
case err != nil:
return fmt.Errorf("failed to read config file: %v: %w", err, ErrRead)
case len(files) == 0:
return fmt.Errorf("no network config found in %s: %w", c.pluginConfDir, ErrCNINotInitialized)
}
// files contains the network config files associated with cni network.
// Use lexicographical way as a defined order for network config files.
sort.Strings(files)
早速辿り着いたのは、containerd
が内部で CNI をキックする時に利用しているライブラリは go-cni
の loadFromConfDir
と言う関数でした。
コメントを読むと分かる通り、設定ファイルディレクトリ内のファイルを辞書順でソートしています。
networks = append(networks, &Network{
cni: c.cniConfig,
config: confList,
ifName: getIfName(c.prefix, i),
})
i++
if i == max {
break
}
そして順番にネットワークを追加し、max値まで追加したらbreak
してますね。
ここで思ったのは、「待て、KubernetesのNICってデフォルト一枚じゃなかったっけ?」でした。
その昔、CNIの記事を書いた際に、戯れにKubernetesが管理するPodに2枚目のNICを刺した ことがあったのですが、containerd
公式にもその方法があったとは。
ソースコードをcontainerd
に戻り、このmax値を制御している設定が、config.toml
の max_conf_num
と言うことがわかりました。と、言うことで冒頭の "TL;DR" に戻るわけですね!
Pod に複数のNICを刺す
そんなわけでせっかくなので、各ノードに上記の設定を施し、実際にPodにネットワークが複数設定されるか試して見ましょう。今回は、以下のCNIの組み合わせを試してみます。
- Flannel + Cilium
- Flannel + Bridge
各CNIの設定は以下の通りです。
- Flannel
- Version
- flannelcni/flannel: v0.20.2
- flannelcni/flannel-cni-plugin: v1.2.0
- Tunnel: none (host-gw)
- IPAM: Kubernetes
- Pod CIDR:
10.242.0.0/16
- Version
- Cilium
- Version
- Chart: 1.12.2
- CLI: v0.12.10
- Tunnel: none (host-gw)
- IPAM: cluster-pool
- Pod CIDR:
172.20.0.0/16
- Version
- Bridge
- name: br0
- CIDR:
192.168.0.0/16
- 備考: ホストネットワーク直結
Flannel + Cilium
まずは、以下のように三つのPodがFlannelによりIPアドレスが割り当てられているとします。
$ kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME IP
testapp-0 10.242.4.2
testapp-1 10.242.1.2
testapp-2 10.242.3.2
コンテナの中を見てみると、eth0 に上記のアドレスがついていることが確認できます。
$ kubectl exec -ti testapp-0 -- ip addr
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
2: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 32:27:88:90:7f:90 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.242.4.2/24 brd 10.242.4.255 scope global eth0
valid_lft forever preferred_lft forever
さて、そこに Cilium をインストールしてみましょう。
以下の kustomization.yaml
を用意します。
$ cat <<EOF > kustomization.yaml
helmCharts:
- name: cilium
includeCRDs: true
valuesInline:
tunnel: disabled
autoDirectNodeRoutes: true
ipv4NativeRoutingCIDR: "172.20.0.0/16"
cni:
exclusive: false
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4PodCIDRList:
- "172.20.0.0/16"
hubble:
enabled: false
releaseName: cilium
version: 1.12.2
repo: https://helm.cilium.io/
patchesStrategicMerge:
- |-
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: cilium
spec:
template:
spec:
containers:
- name: cilium-agent
env:
- name: CNI_CONF_NAME
value: 20-cilium.conf
EOF
Cilium には古いCNIを /etc/cni/net.d
から削除してしまう勝手機能があるのですが、cni.exclusive=false
にしてそれを無効化してやります。
また、デフォルト Cilium がインストールする CNI の設定ファイルのファイル名は 05-cilium.conf
なのですが、事前にインストールされている Flannel の設定ファイル名が 10-flannel.conflist
のため、そのままCNIを適用すると Cilium が優先されてしまい、一番目のNICが Cilium になってしまいます。
今回は、一番目のNICにFlannelを、二番目のNICにCiliumを利用したかったため、CNI_CONF_NAME
を変更して 20-cilium.conf
としています。
さて、早速 Cilium を kustomize build してインストールしてみましょう。
$ kustomize build --enable-helm --helm-command=helm . | k apply -f -
この状態で testapp-0
と testapp-1
を再起動します。
$ kubectl delete pod testapp-0 testapp-1
pod "testapp-0" deleted
pod "testapp-1" deleted
$ kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME IP
testapp-0 10.242.4.5
testapp-1 10.242.1.5
testapp-2 10.242.3.2
その後、PodのIPアドレスを確認してみると、、変わらずFlannelがIPアドレスを割り当てていますね。
Podの中に入ってアドレスを確認してみます。
$ kubectl exec -ti testapp-0 -- ip addr
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: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ba:35:96:c8:e5:49 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.242.4.5/24 brd 10.242.4.255 scope global eth0
valid_lft forever preferred_lft forever
15: eth1@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 76:5e:de:13:57:a5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.20.1.3/32 scope global eth1
valid_lft forever preferred_lft forever
おお、eth1
が追加で設定されているようです。
ルーティングは以下のようになっていました。
$ kubectl exec -ti testapp-0 -- ip route
default via 10.242.4.1 dev eth0
default via 172.20.1.247 dev eth1 mtu 1500
10.242.0.0/16 via 10.242.4.1 dev eth0
10.242.4.0/24 dev eth0 proto kernel scope link src 10.242.4.5
172.20.1.247 dev eth1 scope link
以下の二つのルーティングはFlannelの設定ファイル由来ですね。
- default via 10.242.4.1 dev eth0
- 10.242.0.0/16 via 10.242.4.1 dev eth0
Flannel の設定ファイルをノードで確認してみます。
$ cat /etc/cni/net.d/10-flannel.conflist
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true,
"isGateway": true
}
}
]
}
ここで、isDefaultGateway
と isGateway
が false
になっていると上記のルーティングがなくなっていることが確認できるはずです。
FlannelのネットワークへのルーティングはFlannelが設定したNIC越しに、CiliumへのネットワークへのルーティングはCiliumが設定したNIC越しに利用する、ということがやりたければこのあたりの値を調整する必要がありそうです。
Flannel + Bridge
TBD (自宅のネットワークを調整したらあとで書きたい)
ユースケース
と、まあ、簡単にNICを2枚刺すことはできたのですが、ユースケースは限られそうです。
というのも、NICを2枚刺したいケースというのは、Kubevirt を利用して Kubernetes上のVMにKubernetesのPod Network外のNICを刺したい場合、とか、Longhornでストレージのネットワークを分けたい場合、とかが考えられるのですが、どちらもネイティブに対応しているのは Multus-CNI だったりします。
苦肉の策で思いついたユースケースが CNI の Migration に使えそう、というものだったのですが、これはまた"移行期間中にPod間の接続でNATを許し"さえすれば、このように二つのCNIを共存させる必要はなさそうでした。
CNI の Migration については Kubernetes Advent Calender 2022 の21日目に書く予定です。
まとめ
KubernetesのPodに複数のNICを構成するのは、Multusを使わずとも簡単にできることがわかったが、ユースケースが見当たらない。
以上。