CNI プラグインを手作業で利用する

Kubernetes 関連技術を自動化の流れに反して手作業でやってみよう！シリーズ2回目です。1回目は「[Kubernetes の Service type LoadBalancer を手作業で作る](https://qiita.com/yuanying/items/704a173033410d882eea)」 でした。(シリーズのつもりはない。

## CNI プラグインを手作業で利用する

Kubernetes でコンテナのネットワークインタフェースを設定するために使われている `CNI`。その仕様も使い方も単純な割に意外とその詳細を知ってる人が少ない感じがしたのでこのドキュメントを記す。日本語で。

(英語が読める人はそのまんま [仕様](https://github.com/containernetworking/cni/blob/master/SPEC.md) を読むのが早いと思います。)

## CNI とは！

簡単にいうと、コンテナのネットワークインタフェースを設定するための、`runtime` と `plugin` 間のインタフェースです。

ここでいう `runtime` は例えば Kubernetes とか Apache Mesos になるわけです。で、今回のこの記事の趣旨は、 `runtime` として Kubernetes の代わりに **人** がそのインタフェースを使って `plugin` とやりとりし、あわよくばコンテナにネットワークインタフェースをブッ刺してやろう、そういうことになります。

## コンテナとは！

Linux Container とは？ということにちゃんと答えようとすると色々面倒だったりするのですが、ことネットワーク、CNI の仕様が気にする Linux Container とは何か、というのは簡単です。要するに network namespace のことです。CNI は単純化すると network namespace に eth0, eth1 を追加するための仕様、プラグインはその実装、っていうことになります。

## CNI プラグインとは！

ざっくりいうと CNI を実装したプラグインは以下のようなものだそうで。

1.  実行可能なファイルであること。
2.  コンテナをネットワークに参加させる operation を実装していること。
3.  コンテナをネットワークから外す operation を実装していること。
4.  サポートする CNI version を表示できること。

Kubernetes はこの CNI プラグインを実行可能ファイルとして実行して、コンテナ/Pod にネットワークインタフェースを刺したり外したりしている、ということになります。

## 前提

それでは早速試して見ましょう、と行きたいところなんだけれども折角試すのだから既存の Kubernetes の pod network に自分で作ったコンテナを参加させたりしてみたい、ということで以下のような環境が準備されているものとします。

-   2 node の Kubernetes クラスター。
    -   Node01: 172.18.201.121
    -   Node02: 172.18.201.122
-   CNI プラグインとして Flannel がすでにセットアップされている。
    -   Pod network: 10.244.0.0/16
    -   ついでに 10.244.5.64 というアドレスを持った Pod が起動している。
-   CNI プラグインは `/opt/cni/bin` にインストールされている。

![Kubernetes Cluster Network](https://qiita-image-store.s3.amazonaws.com/0/9958/c9ab9031-ccc0-4886-b523-9040034a3027.png)

## コンテナにネットワークインタフェースを刺す

Node01 にいる Pod とこれから作るコンテナ間で `Ping` が届くか試して見たいので、Node02 にログインしてコンテナを作って見ましょう。

```bash
$ ssh core@172.18.201.121
```

CNI でいうコンテナとは単なる network namespace のことでした。ではコンテナを作ります。

```bash
$ sudo su -
# contid=test-container
# netns=/var/run/netns/$contid
# ip netns add $contid
```

できました。

CNI の仕様では引数を環境変数として設定して起動するので、環境変数を設定します。

```bash
export CNI_PATH=/opt/cni/bin
export CNI_COMMAND=ADD
export PATH=$CNI_PATH:$PATH
export CNI_CONTAINERID=$contid
export CNI_NETNS=$netns
export CNI_IFNAME=eth0
```

では、プラグインを実行しましょう。今回は flannel プラグインを実行して見ます。設定ファイルは標準入力で入力するようです。

```bash
# /opt/cni/bin/flannel <<EOF
> {
>   "name": "cbr0",
>   "type": "flannel",
>   "delegate": {
>     "isDefaultGateway": true
>   }
> }
> EOF
{
    "cniVersion": "0.2.0",
    "ip4": {
        "ip": "10.244.1.5/24",
        "gateway": "10.244.1.1",
        "routes": [
            {
                "dst": "10.244.0.0/16",
                "gw": "10.244.1.1"
            },
            {
                "dst": "0.0.0.0/0",
                "gw": "10.244.1.1"
            }
        ]
    },
    "dns": {}
}
```

何やら帰ってきました。早速コンテナにログインして確認してみます。

```bash
# ip netns exec $contid bash
# ip addr show
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
3: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 0a:58:0a:f4:01:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.1.5/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::6057:23ff:fec4:1db6/64 scope link
       valid_lft forever preferred_lft forever
```

IP アドレスがついてますね、素晴らしい！Node01 の Pod に `Ping` は通るかしら。

```bash
# ping 10.244.5.64
PING 10.244.5.64 (10.244.5.64) 56(84) bytes of data.
64 bytes from 10.244.5.64: icmp_seq=1 ttl=62 time=0.449 ms
64 bytes from 10.244.5.64: icmp_seq=2 ttl=62 time=0.283 ms
64 bytes from 10.244.5.64: icmp_seq=3 ttl=62 time=0.353 ms
^C
--- 10.244.5.64 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2076ms
rtt min/avg/max/mdev = 0.283/0.361/0.449/0.071 ms
```

完璧です。

後片付けをしましょう。

```bash
# export CNI_COMMAND=DEL
# /opt/cni/bin/flannel <<EOF
{
  "name": "cbr0",
  "type": "flannel",
  "delegate": {
    "isDefaultGateway": true
  }
}
EOF
# ip netns del $contid
```

## Kubernetes が管理する Pod に2枚目の NIC を刺す

こうなってくると調子に乗りたくなってきます。CNI の仕様ではどうやらコンテナに NIC は何本でも刺さるようです。ということは、Kubernetes が管理する Pod も所詮は (自分が作った環境では) Docker コンテナの集合なので、その network namespace に NIC をブッ刺すことなんてさっきやったのと同じくらい簡単なはず。

まずは Pod の network namespace を取得しましょう。Pod の network namespace は pause コンテナーのものを共有している、ということは Kubernetes をやってる人ならみんなが知ってる常識です。適当な pause コンテナーの docker container id をゲットします。

```bash
# pause_contid=$(docker ps | grep pause | cut -f1 -d' ' | head -n1)
```

次に、pause コンテナの PID から network namespace を探し当てます。

```bash
# pid=$(docker inspect -f '{{ .State.Pid }}' $pause_contid)
# netns=/proc/$pid/ns/net
```

ここまできたら、あとは先ほどと手順は同じです。flannel プラグインをキックしてみましょう。まずは環境変数の設定、先ほどと違うのは NIC の名前が eth1 になっていることくらい。(すでに Kubenetes が eth0 を刺してるので)

```bash
export CNI_PATH=/opt/cni/bin
export CNI_COMMAND=ADD
export PATH=$CNI_PATH:$PATH
export CNI_CONTAINERID=$contid
export CNI_NETNS=$netns
export CNI_IFNAME=eth1
```

そして、flannel を実行。

```bash
# /opt/cni/bin/flannel <<EOF
> {
>   "name": "cbr0",
>   "type": "flannel",
>   "delegate": {
>     "isDefaultGateway": false
>   }
> }
> EOF
{
    "cniVersion": "0.2.0",
    "ip4": {
        "ip": "10.244.1.6/24",
        "gateway": "10.244.1.1",
        "routes": [
            {
                "dst": "10.244.0.0/16",
                "gw": "10.244.1.1"
            }
        ]
    },
    "dns": {}
}
```

どうやらついたようです。。。

先ほどの pause コンテナから対応する Pod を調べて、Pod にログインしてみましょう。

```bash
$ kubectl exec -ti hmd0-hackmd-7df97457-8gt7g sh
/hackmd $ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
3: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 0a:58:0a:f4:01:07 brd ff:ff:ff:ff:ff:ff
    inet 10.244.1.7/24 scope global eth0
       valid_lft forever preferred_lft forever
7: eth1@if22: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 0a:58:0a:f4:01:06 brd ff:ff:ff:ff:ff:ff
    inet 10.244.1.6/24 scope global eth1
       valid_lft forever preferred_lft forever
```

おー、2枚目の NIC がついとる！

忘れず後片付けを。

```bash
# export CNI_COMMAND=DEL
# /opt/cni/bin/flannel <<EOF
{
  "name": "cbr0",
  "type": "flannel",
  "delegate": {
    "isDefaultGateway": false
  }
}
EOF
```

CNI の仕様によると NIC を刺した順に抜かなければならないようなので、その点を注意しなければならないかも。

以上でした。仕様を知ってると色々遊べそうですね！