CNCF CNI プラグイン

[CNCF](https://www.cncf.io)で管理されているプロジェクトであり、コンテナへのネットワーク接続を実現する技術としてデファクトになりつつある[CNI](https://github.com/containernetworking)について完全に理解したので解説していく。

# CNI 役割

ContainerNetworkInterface(CNI)はコンテナとネットワークを接続する技術。CNIプロジェクト自体は仕様といくつかの基本的なプラグインを提供するのみで、コンテナを利用するユーザがCNI仕様にそったプラグインを自分たちで用意する。

<img width="558" alt="figure1.png" src="https://qiita-image-store.s3.amazonaws.com/0/216087/e51f2278-950f-11e8-7551-8327b68b22b8.png">


# CNI 仕様

CNIの仕様は[Githubレポジトリのドキュメント](https://github.com/containernetworking/cni/blob/master/SPEC.md)にまとまっている。CNIプロジェクトは[containernetworking](https://github.com/containernetworking)配下に2つのリポジトリを持っており、[cni](https://github.com/containernetworking/cni)が仕様やCNIのベースとなる基本的なコードを管理し、[plugins](https://github.com/containernetworking/plugins)は基本的ないくつかのCNIプラグインを管理している。

## CNI 基本的な動作原理
CNIプラグインが実行される際に、ネットワークの設定、またCNIの設定がプラグイン側に渡される。ネットワークの設定は標準入力で、CNIの設定は環境変数を通じてプラグイン側に渡される。プラグインは、渡された情報に基づいて命令を実行し、結果を標準出力に返すというのが一連の動作になる。ちなみにエラー時もエラー結果は標準出力に出力する。

<img width="1064" alt="figure2.png" src="https://qiita-image-store.s3.amazonaws.com/0/216087/e4cf601c-b95d-0542-8df0-df38ba8c7ca5.png">

## CNI オペレーション

CNIプラグインがサポートすべきオペレーションについて紹介する。なおこの情報はCNI version 0.4.0の仕様に基づいて記述されている。

### ADD
コンテナへネットワークを追加する命令。

- 命令実行時に必要な情報
    - コンテナID: コンテナのID
    - ネットワークnamespaceのパス: コンテナのネットワークnamespaceのパス
    - ネットワークの設定: コンテナに追加するネットワークの情報、JSONで記述
    - 拡張情報: CNIプラグインの基本的な設定情報以上の情報を与えるためのもの
    - コンテナ内のインターフェースの名前: コンテナ内で作られるネットワークインターフェースの名前(例: eth0)
- 命令実行後の結果として期待する情報
    - インターフェースのリスト: プラグインが用意したインターフェースのリスト
    - インターフェースごとのIP設定: 上記のインターフェースごとのIP設定
    - DNS情報: DNSの情報

### DEL
コンテナからネットワークを削除する命令。

- 命令実行時に必要な情報
    - コンテナID: コンテナのID
    - ネットワークnamespaceのパス: コンテナのネットワークnamespaceのパス
    - ネットワークの設定: コンテナに追加するネットワークの情報、JSONで記述
    - 拡張情報: CNIプラグインの基本的な設定情報以上の情報を与えるためのもの
    - コンテナ内のインターフェースの名前: コンテナ内で作られるネットワークインターフェースの名前(例: eth0)

### GET
コンテナのネットワーク設定を取得する命令。

- 命令実行時に必要な情報
    - コンテナID: コンテナのID
    - ネットワークnamespaceのパス: コンテナのネットワークnamespaceのパス
    - ネットワークの設定: コンテナに追加するネットワークの情報、JSONで記述
    - 拡張情報: CNIプラグインの基本的な設定情報以上の情報を与えるためのもの
    - コンテナ内のインターフェースの名前: コンテナ内で作られるネットワークインターフェースの名前(例: eth0)
- 命令実行後の結果として期待する情報
    - インターフェースのリスト: プラグインが用意したインターフェースのリスト
    - インターフェースごとのIP設定: 上記のインターフェースごとのIP設定
    - DNS情報: DNSの情報

### VERSION

- 命令実行後の結果として期待する情報
    - cniバージョン: CNIのバージョン
    - サポートしているバージョン: プラグインがサポートしているCNIバージョン

## CNI 環境変数

CNIプラグイン実行時にプラグインに渡すCNIに関連した設定

- CNI_COMMAND: CNIのオペレーション、ADD、DEL、GET、VERSIONのいずれか（version 0.4.0）
- CNI_CONTAINERID: コンテナID
- CNI_NETNS: ネットワークnamespaceのファイルパス
- CNI_IFNAME: コンテナ内のインターフェースの名前
- CNI_ARGS: プラグイン実行時に設定できる任意の情報、Key-value pairをセミコロンで区切って与える（例:"FOO=BAR;ABC=123"）
- CNI_PATH: CNIプラグインの格納しているパスのリスト、Linuxは":"で区切り、Windowsは";"で区切って与える

## ネットワーク設定
プラグインによって様々なパラメータが定義されているが、基本的なものは下記。

- cniVersion (string): CNIのバージョン
- name (string): ネットワークの名前
- type (string): 実行するCNIプラグインのファイル名
- args (dictionary): ランタイムによって与えられるオプション情報
- ipMasq (boolean): オプション。IPマスカレードを設定
- ipam: IPAM設定のための情報
    - type (string): IPAMプラグインのファイル名
- dns: DNS設定のための情報
    - nameservers (list of strings): DNS nameserverのリスト。IPv4もしくはIPv6で表現されたstringのリスト
    - domain (string): ローカルドメインの指定
    - search (list of strings): サーチドメインのリスト
    - options (list of strings): リゾルバに渡すオプションのリスト

# CNI プラグインを書いてみる
CNIの仕様は上で述べた通りなので、仕様に沿うように実装すればどのように書いても実現できる。CNIプラグインはGo言語で書かれていることが多いが、どのような言語でも簡単に実現できるし、なんならBashでもシンプルに書くことが出来る。シンプルというか、

```bash
#!/usr/bin/env bash
echo '{}'
```

なんとこれだけでも動く。では、実際に試していこう。CNIプラグインを簡単に試すことが出来るツール[cnitool](https://github.com/containernetworking/cni/tree/master/cnitool)があるので、これを使っていく。シミュレーションするツールだけ？と思うかもしれないが、ちゃんとKubernetesでも動くことを確認しているのでご心配なく。

CNIプラグイン本体の準備。宣言通りに中身は`echo`のみ。

```bash
$ sudo mkdir -p /opt/cni/bin/
$ sudo cat << EOL > /opt/cni/bin/cnisample.sh
> #!/usr/bin/env bash
> echo '{}'
> EOL
```

実行権限を与えて終わり。

```bash
$ sudo chmod +x /opt/cni/bin/cnisample.sh
```

次にCNI設定ファイルの準備。

```bash
$ sudo mkdir -p /etc/cni/net.d/
$ sudo echo '{"cniVersion":"0.4.0","name":"mysample","type":"cnisample.sh"}' > /etc/cni/net.d/10-mysample.conf
```

`cnitool`を使う準備。

```bash
$ go get github.com/containernetworking/cni/cnitool
$ sudo ip netns add testing
```

これで準備完了。実行する。CNIプラグインを置いた`CNI_PATH`を指定する必要がある。CNIプラグインの設定ファイルのパスはデフォルトで`/etc/cni/net.d/`となっているので指定する必要はないが、任意の場所に置いた場合は`NETCONFPATH`で指定する。

```bash
$ sudo CNI_PATH=/opt/cni/bin cnitool add mysample /var/run/netns/testing
{
    "dns": {}
}
```

成功した（特に設定していない`"dns":{}`だけ出力されるのが少し気持ち悪いのでcniのコードを読んだが、どうやら仕様らしい）。このように中身なくても成功してしまうが、流石に仕様も何もないので、CNIスペックにある程度そったシェルスクリプトも用意した。

[リポジトリ](https://github.com/hichihara/cnisample)に置いたのでgit cloneして準備。

```bash
$ git clone https://github.com/hichihara/cnisample
$ cd cnisample
```

利用するのは`bash/cnisample.sh`、下記がコードの中身。

```bash
#!/usr/bin/env bash

count=1
interfaces='{"name":"sample-interface'$count'"}'
ips='{"version":"4","address":"10.0.0.'$count'/32","interface":'$count'}'

if [ "$CNI_COMMAND" == "ADD" ]; then
    echo '{"cniVersion":"0.4.0","interfaces":['$interfaces'],"ips":['$ips'],"dns":{}}'
fi

if [ "$CNI_COMMAND" == "DEL" ]; then
    echo '{}'
fi

if [ "$CNI_COMMAND" == "VERSION" ]; then
    echo '{"cniVersion": "0.4.0", "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ]}'
fi

if [ "$CNI_COMMAND" == "GET" ]; then
    echo '{"cniVersion":"0.4.0","interfaces":['$interfaces'],"ips":['$ips'],"dns":{}}'
fi
```

環境変数の`CNI_COMMAND`でどのようなオペレーションかチェックして、仕様に従って適当な値を埋めた結果を標準出力で出力している。

ADDとDELの実行。

```bash
$ sudo CNI_PATH=./bash/ NETCONFPATH=./bash/netconf/cnisample cnitool add mysample /var/run/netns/testing
{
    "cniVersion": "0.4.0",
    "interfaces": [
        {
            "name": "sample-interface1"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 1,
            "address": "10.0.0.1/32"
        }
    ],
    "dns": {}
}
$ sudo CNI_PATH=./bash/ NETCONFPATH=./bash/netconf/cnisample cnitool del mysample /var/run/netns/testing
```

終わったらnetwork namespaceを削除してゴミ掃除。

```bash
$ sudo ip netns del testing
```

# CNI チェイン機能
CNIは基本機能としてプラグインをチェイン実行、つまり一回のオペレーションで複数のプラグインを連続して実行する機能を持っている。

## チェイン設定

ネットワーク設定に`plugins`を追加し、各プラグインの設定をリストで`plugins`配下に記述する。チェイン時に重要になるのが`prevResult`というパラメータで、チェイン実行されていくプラグインの呼び出し時に与えられるネットワーク設定に、前のプラグインの実行結果を`prevResult`パラメータに入れて渡すことができる。

<img width="1047" alt="figure3.png" src="https://qiita-image-store.s3.amazonaws.com/0/216087/accaf7d3-d62c-3569-0e5f-613720ab23a9.png">


## チェイン機能を試す

上の章で紹介したリポジトリに`bash/cnisample-chain.sh`という名前でチェイン用のスクリプトも用意したので試すことができる。このスクリプトは内部で`jq`を使用するのでお使いの環境で事前にインストールしておく。

リポジトリの準備。

```bash
$ git clone https://github.com/hichihara/cnisample
$ cd cnisample
```

network namespaceの準備。

```bash
$ sudo ip netns add testing
```

利用するのは`bash/cnisample-chain.sh`、下記がコードの中身。

```bash
#!/usr/bin/env bash

count=1

if [ -p /dev/stdin ]; then
    buf=$(cat -)
    previnterface=$(echo $buf | jq -r '.prevResult.interfaces[].name')
    previps=$(echo $buf | jq -r '.prevResult.ips[]')
    count=$((++count))
    interfaces='{"name":"'$previnterface'"},{"name":"sample-interface'$count'"}'
    ips=''$previps',{"version":"4","address":"10.0.0.'$count'/32","interface":'$count'}'
fi

if [ "$CNI_COMMAND" == "ADD" ]; then
    echo '{"cniVersion":"0.4.0","interfaces":['$interfaces'],"ips":['$ips'],"dns":{}}'
fi

if [ "$CNI_COMMAND" == "DEL" ]; then
    echo '{}'
fi

if [ "$CNI_COMMAND" == "VERSION" ]; then
    echo '{"cniVersion": "0.4.0", "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ]}'
fi

if [ "$CNI_COMMAND" == "GET" ]; then
    echo '{"cniVersion":"0.4.0","interfaces":['$interfaces'],"ips":['$ips'],"dns":{}}'
fi
```

`cnisample.sh`との差分は標準入力からネットワーク設定を受け取っている点。特にネットワークの設定をする必要もないので`cnisample.sh`で受け取らなかったが、チェインでは`prevResult`を受け取ることができるので、せっかくなので受け取って処理に使用している。

`bash/netconf/cnisample-chain/10-mysample.conflist`にチェイン用の設定ファイルを置いてある。

```json
{
  "cniVersion": "0.4.0",
  "name": "mysample",
  "plugins": [
    {
      "type": "cnisample.sh"
    },
    {
      "type": "cnisample-chain.sh"
    }
  ]
}
```

`plugins`配下で`cnisample.sh`と`cnisample-chain.sh`をチェイン実行するように宣言している。

ADDとDELの実行。

```bash
$ sudo CNI_PATH=./bash/ NETCONFPATH=./bash/netconf/cnisample-chain cnitool add mysample /var/run/netns/testing
{
    "cniVersion": "0.4.0",
    "interfaces": [
        {
            "name": "sample-interface1"
        },
        {
            "name": "sample-interface2"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 1,
            "address": "10.0.0.1/32"
        },
        {
            "version": "4",
            "interface": 2,
            "address": "10.0.0.2/32"
        }
    ],
    "dns": {}
}
$ sudo CNI_PATH=./bash/ NETCONFPATH=./bash/netconf/cnisample-chain cnitool del mysample /var/run/netns/testing
```

終わったらnetwork namespaceを削除してゴミ掃除。

```bash
$ sudo ip netns del testing
```