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が存在することによってコンテナネットワークの作成や削除を始めとしたインタフェースを共通化している。 そのおかげでプラグインを実装することによって様々なユースケースに応じたコンテナネットワークを柔軟に構成することができる。