Edited at

CNCF CNI プラグイン

More than 1 year has passed since last update.

CNCFで管理されているプロジェクトであり、コンテナへのネットワーク接続を実現する技術としてデファクトになりつつあるCNIについて完全に理解したので解説していく。


CNI 役割

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

figure1.png


CNI 仕様

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


CNI 基本的な動作原理

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

figure2.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でもシンプルに書くことが出来る。シンプルというか、

#!/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パラメータに入れて渡すことができる。

figure3.png


チェイン機能を試す

上の章で紹介したリポジトリに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.shcnisample-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