CNCFで管理されているプロジェクトであり、コンテナへのネットワーク接続を実現する技術としてデファクトになりつつあるCNIについて完全に理解したので解説していく。
CNI 役割
ContainerNetworkInterface(CNI)はコンテナとネットワークを接続する技術。CNIプロジェクト自体は仕様といくつかの基本的なプラグインを提供するのみで、コンテナを利用するユーザがCNI仕様にそったプラグインを自分たちで用意する。
CNI 仕様
CNIの仕様はGithubレポジトリのドキュメントにまとまっている。CNIプロジェクトはcontainernetworking配下に2つのリポジトリを持っており、cniが仕様やCNIのベースとなる基本的なコードを管理し、pluginsは基本的ないくつかのCNIプラグインを管理している。
CNI 基本的な動作原理
CNIプラグインが実行される際に、ネットワークの設定、またCNIの設定がプラグイン側に渡される。ネットワークの設定は標準入力で、CNIの設定は環境変数を通じてプラグイン側に渡される。プラグインは、渡された情報に基づいて命令を実行し、結果を標準出力に返すというのが一連の動作になる。ちなみにエラー時もエラー結果は標準出力に出力する。
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でもシンプルに書くことが出来る。シンプルというか、
#!/usr/bin/env bash
echo '{}'
なんとこれだけでも動く。では、実際に試していこう。CNIプラグインを簡単に試すことが出来るツールcnitoolがあるので、これを使っていく。シミュレーションするツールだけ?と思うかもしれないが、ちゃんとKubernetesでも動くことを確認しているのでご心配なく。
CNIプラグイン本体の準備。宣言通りに中身はecho
のみ。
$ sudo mkdir -p /opt/cni/bin/
$ sudo cat << EOL > /opt/cni/bin/cnisample.sh
> #!/usr/bin/env bash
> echo '{}'
> EOL
実行権限を与えて終わり。
$ sudo chmod +x /opt/cni/bin/cnisample.sh
次にCNI設定ファイルの準備。
$ 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
を使う準備。
$ go get github.com/containernetworking/cni/cnitool
$ sudo ip netns add testing
これで準備完了。実行する。CNIプラグインを置いたCNI_PATH
を指定する必要がある。CNIプラグインの設定ファイルのパスはデフォルトで/etc/cni/net.d/
となっているので指定する必要はないが、任意の場所に置いた場合はNETCONFPATH
で指定する。
$ sudo CNI_PATH=/opt/cni/bin cnitool add mysample /var/run/netns/testing
{
"dns": {}
}
成功した(特に設定していない"dns":{}
だけ出力されるのが少し気持ち悪いのでcniのコードを読んだが、どうやら仕様らしい)。このように中身なくても成功してしまうが、流石に仕様も何もないので、CNIスペックにある程度そったシェルスクリプトも用意した。
リポジトリに置いたのでgit cloneして準備。
$ git clone https://github.com/hichihara/cnisample
$ cd cnisample
利用するのはbash/cnisample.sh
、下記がコードの中身。
#!/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の実行。
$ 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を削除してゴミ掃除。
$ sudo ip netns del testing
CNI チェイン機能
CNIは基本機能としてプラグインをチェイン実行、つまり一回のオペレーションで複数のプラグインを連続して実行する機能を持っている。
チェイン設定
ネットワーク設定にplugins
を追加し、各プラグインの設定をリストでplugins
配下に記述する。チェイン時に重要になるのがprevResult
というパラメータで、チェイン実行されていくプラグインの呼び出し時に与えられるネットワーク設定に、前のプラグインの実行結果をprevResult
パラメータに入れて渡すことができる。
チェイン機能を試す
上の章で紹介したリポジトリにbash/cnisample-chain.sh
という名前でチェイン用のスクリプトも用意したので試すことができる。このスクリプトは内部でjq
を使用するのでお使いの環境で事前にインストールしておく。
リポジトリの準備。
$ git clone https://github.com/hichihara/cnisample
$ cd cnisample
network namespaceの準備。
$ sudo ip netns add testing
利用するのはbash/cnisample-chain.sh
、下記がコードの中身。
#!/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
にチェイン用の設定ファイルを置いてある。
{
"cniVersion": "0.4.0",
"name": "mysample",
"plugins": [
{
"type": "cnisample.sh"
},
{
"type": "cnisample-chain.sh"
}
]
}
plugins
配下でcnisample.sh
とcnisample-chain.sh
をチェイン実行するように宣言している。
ADDとDELの実行。
$ 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を削除してゴミ掃除。
$ sudo ip netns del testing