CNIの役割
最近のパブリッククラウドを利用している人はあまり意識しないかもしれないが、手動でKubernetesクラスタを構築したことのある人ならCNIという単語は聞いたことがあるだろう。 CNIプラグインはKubernetesであればpodの通信が通るネットワークの管理を行っているが、実際にCNI及びCNIプラグインの厳密な守備範囲について確認する。
CNIとは
CNIはコンテナネットワークに関するインターフェースの仕様を標準化したものである。Kubernetes関連でCNIという単語を聞いたことがある人が多いと思うが、実際はそれに限った話ではない。 コンテナを作成したらコンテナ単体が存在するネットワークなのか、あるいはアプリケーションで利用するコンテナ軍が接続されたものなのか様々あるだろうが、大抵の場合はネットワークに接続したくなるはずだ。
ネットワークの構築やコンテナに対する接続を構築するアプリケーションは一つの機能としてコンテナ構築とは別で外部に切り出されている。 この際にネットワークの定義の仕様を取り決めたものをCNI、実際にネットワークの構築や、コンテナへの接続を担当するアプリケーションをCNIプラグインという。
ここで注意したいのが、コンテナといえばDockerを思い浮かべる人が多いと思うが、Dockerでコンテナを作成した際にアタッチされるネットワークの構築にはCNIは使われておらずContainer Network Model(CNM)という別の概念を実装したlibnetworkが使われている。
すなわちコンテナ技術を使っているからといってネットワーク周りをCNIに準基する必要があるとそうではない。
CNIプラグインには様々な実装があり標準実装としてかんたんなものがこちらに用意されている。 他にはIPIPのトンネリングを利用したコンテナ間通信を行うCalicoやVXLANを利用するflannelなどさまざま存在する。
CNIの仕様
全文はこちらを参照。 CNIにおける用語は以下の通り。 - コンテナ - ネットワークで分断されたドメイン。一般的にはLinuxのnetwork namespaceが用いられるが、仮想マシンを1単位とする場合もある。このようにネットワーク的に分離しているものであればコンテナと呼ぶことになっているので、分離している技術に依存するものではない。 - ネットワーク - 相互に通信でき一意のアドレスで指定可能なエンドポイントをまとめたもの。コンテナは1つ以上のネットワークに対して追加削除できる。 - プラグイン - 指定されたネットワークの構成を担当するプログラム。 - ランタイム - CNIプラグインの実行をするためのプログラム
CNIでは上記の定義を前提に以下の5つの仕様が定義されている。 - ネットワークの構成を指定するためのフォーマット(json形式で構成を指定するが、その書式が定義されている) - https://www.cni.dev/docs/spec/#section-1-network-configuration-format - コンテナランタイムがネットワークプラグインに対してリクエストを送る際のフォーマット - https://www.cni.dev/docs/spec/#section-2-execution-protocol - 定義された構成を基にプラグインを実行するための手順 - https://www.cni.dev/docs/spec/#section-3-execution-of-network-configurations - 必要であればプラグインが他のプラグインに対して処理を委譲するがそのための手順 - https://www.cni.dev/docs/spec/#section-4-plugin-delegation - プラグインが実行結果をランタイムに返す際のフォーマット - https://www.cni.dev/docs/spec/#section-5-result-types
ひとまず全体の流れとしてはこの様になる。
ランタイムがプラグインを呼び出す際には環境変数もしくは標準入力を通した設定値を介してパラメータを設定する。 プラグインはJsonエンコードされた実行結果を標準入力、標準出力に書き込む。 また仕様ではプラグインを実行するネットワークネームスペースはランタイムのそれと同じでなければならないと定められている。
ランタイムがプラグインを実行する際に指定するパラメータいくつかあるが特に注目したいのはCNI_COMMAND
である。
このパラメータはプラグインの挙動を決める大きな役割を担っているており以下の4つのうちどれかが指定される。
ADD
CNI_IFNAME
で定義されたインターフェースをCNI_NETNS
で定義されたコンテナに対して作成するCNI_NETNS
で定義されたコンテナのCNI_IFNAME
で定義されたインターフェースを調整する。
DEL
CNI_IFNAME
で定義されたインターフェースをCNI_NETNS
から削除する。- ADDで適用された内容をもとに戻す
CHECK
- 既存のコンテナが期待通りのステータスになっているかどうかを確認する。
- CHECKはADD処理の後に呼ばれ正常に適用できていることが確認される。
VERSION
- プラグインがサポートするCNIバージョンの取得
CNIの動作確認
CNIの一連の流れを手元で動かして確認する。 containerdやCRI-Oを使ってコンテナを作成すれば、それでCNIが使われていることになるのだが、内部でgo-cniを使っているため、具体的にどういった引数を渡しているかなど追うのがわかりにくい。
CNIで登場するコンテナは説明した通り、Linuxのnetwork namspaceのようにネットワーク的に分断されているものと定義されているので、これを使うことにする。
CNIには標準実装となるプラグインが用意されていいるのでクローンしてビルドし利用する。(GOのビルド環境が必要) - https://github.com/containernetworking/plugins
$ ./build_linux.sh Building plugins bandwidth firewall portmap sbr tuning vrf bridge dummy host-device ipvlan loopback macvlan ptp vlan dhcp host-local static $ ls ./bin/ bandwidth bridge dhcp dummy firewall host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan vrf
まずコンテナ(netwok namespace)を用意する。
$ sudo ip netns add container-1 $ ip netns list container-1
何も追加していない状態のためループバックインタフェースしか見えていない
$ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
ここにCNIを利用してインターフェースを足してみる。 今回はビルドしたプラグインの中から接続したcontainer(namespace)がbridgeを介して接続されるbridge pluginを使ってみる。
ここに記載されているようにCNIの仕様ではいくつかのパラメータを環境変数を介して渡す必要がある。 紹介したCNI_COMMANDもここで渡している。今回はコンテナにインタフェースを追加するためADDを指定した。 ADDを利用するためにはこれらの環境変数を設定する必要がある。
https://www.cni.dev/docs/spec/#add-add-container-to-network-or-apply-modifications - CNI_COMMAND - CNI_CONTAINERID - CNI_NETNS - CNI_IFNAME
$ export CNI_PATH=$(readlink -f ./bin/) $ export CNI_COMMAND=ADD $ export CNI_CONTAINERID=container-1 $ export CNI_NETNS=/var/run/netns/container-1 $ export CNI_IFNAME=eth0
bridge pluginを利用する際のconfig例としてこのように記載されている。 そしてCNIプラグインへconfigを渡す際は標準入力から渡すことになっているので、以下のように実行する。 このとき先程設定した環境変数を渡すためにsudoを利用する場合は に-Eオプションを忘れずに。
$ cat << 'EOF' | sudo -E ./bin/bridge { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } } EOF { "cniVersion": "0.3.1", "interfaces": [ { "name": "mynet0", "mac": "02:3a:b3:d6:cc:a7" }, { "name": "vethf5f1a233", "mac": "5e:51:7c:b8:a9:44" }, { "name": "eth0", "mac": "0e:c0:ab:d5:02:bb", "sandbox": "/var/run/netns/container-1" } ], "ips": [ { "version": "4", "interface": 2, "address": "10.10.0.2/16", "gateway": "10.10.0.1" } ], "routes": [ { "dst": "0.0.0.0/0", "gw": "10.10.0.1" } ], "dns": {}
そうすると上記のように標準出力に実行結果が返される。 そして再びipコマンドでコンテナ内のインタフェースを確認すると先程指定したものが追加されていることがわかる。またbrctlでは指定したbridgeが追加されている。
$ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether d2:40:0f:c1:3f:da brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.10.0.3/16 brd 10.10.255.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d040:fff:fec1:3fda/64 scope link valid_lft forever preferred_lft forever $ brctl show mynet0 bridge name bridge id STP enabled interfaces mynet0 8000.26dc84ed6e49 no veth24b7b96d
指定したルート情報も挿入されている。
[yota@ubuntu01 plugins]$ sudo ip netns exec container-1 route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default _gateway 0.0.0.0 UG 0 0 0 eth0
もう1つコンテナを用意して同様にbdridgeに接続する。
$ sudo ip netns add container-2 $ export CNI_COMMAND=ADD $ export CNI_CONTAINERID=container-2 $ export CNI_NETNS=/var/run/netns/container-2 $ export CNI_IFNAME=eth0 $ cat << 'EOF' | sudo -E ./bin/bridge { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } } EOF { "cniVersion": "0.3.1", "interfaces": [ { "name": "mynet0", "mac": "26:dc:84:ed:6e:49" }, { "name": "vetheb3b55cd", "mac": "5a:7e:be:be:4d:32" }, { "name": "eth0", "mac": "da:dd:d9:9b:83:e6", "sandbox": "/var/run/netns/container-2" } ], "ips": [ { "version": "4", "interface": 2, "address": "10.10.0.4/16", "gateway": "10.10.0.1" } ], "routes": [ { "dst": "0.0.0.0/0", "gw": "10.10.0.1" } ], "dns": {} } $sudo ip netns exec container-2 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether da:dd:d9:9b:83:e6 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.10.0.4/16 brd 10.10.255.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d8dd:d9ff:fe9b:83e6/64 scope link valid_lft forever preferred_lft forever
これでコンテナ間無事疎通が取れることを確認できた。
$ sudo ip netns exec container-2 ping 10.10.0.3 -c3 PING 10.10.0.3 (10.10.0.3) 56(84) bytes of data. 64 bytes from 10.10.0.3: icmp_seq=1 ttl=64 time=0.104 ms 64 bytes from 10.10.0.3: icmp_seq=2 ttl=64 time=0.061 ms 64 bytes from 10.10.0.3: icmp_seq=3 ttl=64 time=0.061 ms $ sudo ip netns exec container-1 ping 10.10.0.4 -c3 PING 10.10.0.4 (10.10.0.4) 56(84) bytes of data. 64 bytes from 10.10.0.4: icmp_seq=1 ttl=64 time=0.145 ms 64 bytes from 10.10.0.4: icmp_seq=2 ttl=64 time=0.077 ms 64 bytes from 10.10.0.4: icmp_seq=3 ttl=64 time=0.073 ms ### メモ 後で消す mynet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 26:dc:84:ed:6e:49 brd ff:ff:ff:ff:ff:ff inet 10.10.0.1/16 brd 10.10.255.255 scope global mynet0 valid_lft forever preferred_lft forever inet6 fe80::2cc7:a5ff:fe41:76d4/64 scope link
なお今回はbridgeを介した通信をするにあたって、iptablesに特定のルールを追加していないため、このあたりのカーネルパラメータを設定している。container間で疎通が取れないようであれば確認したほうがよい。
nsudo sysctl -w net.bridge.bridge-nf-call-iptables = 0 nsudo sysctl -w et.bridge.bridge-nf-call-arptables = 0
最後は作成したプラグイン経由で作成したインタフェースを削除してみる。 DELの実行では以下のパラーメタが必要となる。
https://www.cni.dev/docs/spec/#del-remove-container-from-network-or-un-apply-modifications - CNI_COMMAND - CNI_CONTAINERID - CNI_IFNAME
$ export CNI_COMMAND=DEL $ export CNI_CONTAINERID=container-1 $ export CNI_NETNS=/var/run/netns/container-1 # netwrok namespace指定のために必要 $ export CNI_IFNAME=eth0 cat << 'EOF' |sudo -E ./bin/bridge > { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "ipam": {} } > EOF
このように実行するとステータスコードが0で返り、指定したインタフェースが削除されていることが確認できた。
$ echo $? 0 $ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
まとめ
CNIが存在することによってコンテナネットワークの作成や削除を始めとしたインタフェースを共通化している。 そのおかげでプラグインを実装することによって様々なユースケースに応じたコンテナネットワークを柔軟に構成することができる。