Kuryrについて

OpenStack Summitで結構大々的に発表されてたkuryr(喋ってたのを聴いてるとクリル？（クリア？[参考](https://twitter.com/ritchey98/status/666644795356524545)）と読むっぽい)について、あんまり情報がないので、一番それっぽい公式ドキュメント(2015/11/18現在)の[これ](http://docs.openstack.org/developer/kuryr/devref/index.html)とか[これ](http://docs.openstack.org/developer/kuryr/devref/goals_and_use_cases.html)を読んでみて、物凄い適当に訳すとこんな感じになった。

この記事はOpenStack Summitの発表を聴いて、あるいはその発表内容の記事を基に書いてるわけではないので、今現在は既にちょっと違ってるかもしれない。ドキュメントの内容も構成も時々変わっていて、変更を追いきれてないかもしれない。
しかし、さすがにそんなに大ブレしたりはしないとは思うので、参考までに。

ちなみに今Docker1.9対応で大忙しなのか、実際動かしてみると、色々直さないと動かなかったりするので、評価はもう少し待ってみたほうがいいらしい。

ソースコードは[こちら](https://github.com/openstack/kuryr)。

## Kuryrとは何か

![CS_D8hKWsAIcZOM.jpg](https://qiita-image-store.s3.amazonaws.com/0/45715/8fdb6db9-47e0-8a9d-bf19-7d23d276c9c7.jpeg)


『KuryrはDockerのlibnetwork remote network driverを実装したもので、OpenStack NeutronのAPI呼び出しにマッピングするもの。libnetworkのContainer Networking Model(CNM)とNeutronのネットワークモデルの間の通訳係として働き、コンテナとホスト間、またはコンテナとVM(nested VM)の間のバインディングを提供する。』

Neutronのネットワークモデルについては[こちら](https://wiki.openstack.org/wiki/Neutron/APIv2-specification)、CNMについては[こちら](https://github.com/docker/libnetwork/blob/master/docs/design.md#the-container-network-model)や[こちら](https://blog.docker.com/2015/04/docker-networking-takes-a-step-in-the-right-direction-2/)が参考になるが、一応以下にざっくり書いておく。

### Neutronのネットワークモデル

![neutron.png](https://qiita-image-store.s3.amazonaws.com/0/45715/057813bd-5492-1856-21c9-19ae0de6aa4e.png)

(この図は色々なのを参考に適当に描いたので合ってないかも)

* Network
  * Networkは仮想的に隔離されたL2ブロードキャストドメインを指す
  * 明示的に"共有"と設定しない限り、作成したTenantに属する
  * Tenantは、Tenantあたりの上限Network数まで、複数のNetworkを作ることができる
  * NetworkはNeutron APIの主要なエンティティ
  * PortとSubnetは必ずNetworkに紐付いてる
* Subnet
  * SubnetはVMに付与するIPが所属すするIPアドレスブロックを表現するもの
  * 各Subnetは必ずCIDRを持ち、Networkに関連付けられている必要がある
  * IPアドレスはSubnetのCIDRから選択されるか、ユーザの指定する"allocation pools"から割り当てられる
  * SubnetはGateway(gateway_ip)、DNSサーバ(dns_servers)、ホストのRoute(host_routes)を持ち得る
* Port
  * Portは、仮想スイッチ上のスイッチポートを表現するもの
  * VMは、自身のNICをそのポートと接続する
  * Portは、そのPortに接続するNICに関連付けられるMACアドレスとIPアドレスも定義する
  * Portは、暗にSubnetに関連付き、IPアドレスはSubnetのallocation poolから取得される

あと何かこれらに付随する細かい要素が色々あるが割愛。

### Container Networking Model

![Container Networking Model](https://blog.docker.com/media/2015/04/cnm-model.jpg)

(こっちの図は本家から引用)

* Sandbox (Network Sandbox)
  * DockerコンテナのネットワークスタックやNICを隔離する環境
  * Sandboxは複数のNetworkに繋がる複数のEndpointを持ち得る
  * **要するにnetns**
* Endpoint
  * 特定ネットワークで通信するためのNIC
  * Endpointは1つのNetworkに参加
  * **要するにveth**
* Network
  * 一意に識別できる、お互いが通信できるEndpointの集合
  * 例えばFrontendとかBackendというネットワークを作ることができて、それはお互い完全に隔離される
  * **要するにbridgeとかVLAN**

Container Networking ModelはNetworkとコンテナの間における以下の約束事を提供する。

* 同じNetworkの全てのコンテナは自由にお互い通信できる
* 複数Networkはコンテナ間のトラフィックを分ける手段であり、全てのドライバでサポートされるべき
* コンテナ毎の複数Endpointは、コンテナが複数のNetworkに所属するための手段
* Endpointは、そのEndpointへの接続性提供のためにNetwork Sandboxに追加される

## 目標および目的

これは何が嬉しいのか。

『Kuryrを通すことで、あらゆるNeutronプラグインは、余計な苦労をしなくてもlibnetworkのバックエンドとして使える事になる。Neutron API自体はベンダ非依存なので、libnetworkから一旦Neutronを通せば、ちょっとした軽量スニペット（多分VIF Bindingのことで、原文にnovaがどうこう、と書いてあるのは多分novaのVIF driverの話）で色々なNeutronプラグインをDockerネットワーキングのバックエンドとしても使えるようになる』よね、というもの。

(docker -> libnetwork -> kuryr (と軽量スニペット) -> neutron -> 各種プラグイン -> 欲しい仮想ネットワーク、という感じ？)

『上述のとおり、Kuryrはコンテナのnetns内外を繋ぐVirtual Ethernet Pair(veth)の片方を、VIF Bindingによってホスト側のPort(Linux Bridge Portとか、OVS PortとかMidonet Portなど)にバインドする点についても面倒を見る。』

"Endpointは、そのEndpointへの接続性提供のためにNetwork Sandboxに追加される" を満たす上でも必須だし、これがないとプラグインで作った仮想ネットワークにコンテナをくっつけられないし、それもそうかと考えられる。

## ユースケース

『Kuryrはコンテナネットワーキングの観点でOpenStack Magnumのユースケースに取り掛かるべきで、Magnumだけでなく、コンテナ活用を必要とする他のOpenStackプロジェクトへの、Neutron APIを通したコンテナネットワーキングの統一的なインタフェースとして動作すべき。この辺を考えると、KuryrはVM nested コンテナのユースケースをサポートするNeutronプラグインの活用や、それらのケースをサポートするためのNeutron APIの強化(OVNとか)を目指している。』

要するに、各所で言われてるとおり、将来的にOpenStackで扱うVM、VM上のコンテナなどのネットワークを一緒に扱う（API統一的にも、ネットワークへの所属的にも）ことができればいいですね、とかいうことだと考えられ、

OpenStack -> (コンテナの場合：Magnumとか -> docker -> libnetwork -> kuryr (と軽量スニペット)) -> neutron -> 各種プラグイン -> コンテナ、VM関わらず欲しい仮想ネットワーク

という感じ？

今のところMagnumで作られたコンテナクラスタのネットワークはNeutronと連携していないが、そのうちKuryrによってさりげなくNeutron管理に入るんだろうと考えられる。

## Kuryrワークフロー

『Kuryrはdockerコンテナが実行されるそれぞれのホストに存在し、libnetwork remote driverに要求されるAPI実装として動く。Neutronの新しい特徴として、ユーザによってNeutronのリソースにtagを付けられるというのがあるが、kuryrではそれをNeutronのリソースIDとDockerのID(UUID)をマッピングするために使う予定？』
※今ソースを見てもそういうのは入ってないように見える

1. 『libnetworkは[plugin discovery mechanism](https://github.com/docker/docker/blob/master/docs/extend/plugin_api.md#plugin-discovery)を経由してkuryrを見つける』  
  ※この処理中に、libnetworkは/Plugin.ActiveにHTTP POSTを行い、それがnetwork driverかどうかをチェックする
2. 『Kuryrをlibnetworkのremote driverとして登録する』
3. 『ユーザは、libnetworkに対してドライバをkuryrにするよう指定してリクエストを作成する』  
  ※--driver=kuryr とか、 -d kuryrをdockerコマンドにつける
4. 『kuryrがリクエストを受け取ると、Neutron clientを使ってNeutron APIを呼び出す』
5. 『Neutronからレスポンスを受け取って、libnetworkへのレスポンスに変換する』
6. 『libnetworkに返却』
7. 『libnetworkはその返却された情報をKV(libkvのバックエンド)に格納する』


## libnetworkとユーザのワークフロー (kuryrをremote network driverとして使う) - ホストネットワーキング

ネットワークの作成、コンテナの追加や削除、ネットワークの削除をした時の処理を見てみる。

１. 『ユーザがfooというdockerのネットワークを作るとする』

『remote driverにはkuryrを指定する。』

```bash
$ sudo docker network create --driver=kuryr foo
286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364
```

『このコマンドを実行すると、以下の様なjsonのリクエストが/NetworkDriver.CreateNetworkに対してPOSTで発行される。』

```json
{
    "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
    "IPv4Data": [{
        "Pool": "172.18.0.0/16",
        "Gateway": "172.18.0.1/16",
        "AddressSpace": ""
    }],
    "IPv6Data": [],
    "Options": { "com.docker.network.generic": {}}
}
```

『Kuryr remote network driverは、下地になるNeutronのネットワークを作るため、Neutron APIリクエストを生成する。
NeutronでNetworkが作られると、Kuryr remote network driverは空の成功レスポンスをdockerデーモンに対して生成して、NeutronのNetworkにDockerのNetworkIDでタグ付けする。』

２. 『ユーザがさっき作ったfooというネットワークでDockerコンテナを起動する』

```bash
$ sudo docker run --net=foo -itd --name=container1 busybox
78c0458ba00f836f609113dd369b5769527f55bb62b5680d03aa1329eb416703
```

『このコマンドを実行すると、以下の様なjsonのリクエストが/NetworkDriver.CreateEndpointに対してPOSTで発行される。』

```json
{
    "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
    "Interface": {
        "AddressIPv6": "",
        "MacAddress": "",
        "Address": "172.18.0.2/16"
    },
    "Options": {
        "com.docker.network.endpoint.exposedports": [],
        "com.docker.network.portmap": []
    },
    "EndpointID": "edb23d36d77336d780fe25cdb5cf0411e5edd91b0777982b4b28ad125e28a4dd"
}
```

『この後、Kuryr remote network driverはNeutron subnetとportを作成するためのNeutron APIへのリクエストを、リクエスト内のinterfaceへのマッチングフィールドと合わせて作成する。
Kuryrは、interfaceのIPについて何も情報を載せないように動的にsubnetを作成する必要がある。』

『以下の様な手順を踏む。』

(1) 『Endpointの作成時、Kuryrは要求されたAddressかIPv6Addressに対応するCIDRのsubnetが存在するかどうかをチェックする』
(2) 『既にsubnetがあれば、Kuryrは新しいsubnetを作らないで再利用しようとするし、なければ与えられたCIDRの新しいsubnetを作る』
(3) 『Kuryrはportを作り、IPを割り当て、(2)で作成したsubnetに関連付ける』
(4) 『Neutron subnetとportに、EndpointIDでタグ付けする』

『上記(2)と(3)のsubnetの作成で、Kuryrはallocation_poolを特定せず、とにかくallocation poolを掴もうとする。allocation_poolなしだと、Neutron APIリファレンスに書かれてるとおり、Neutronは全てのIPをsubnetのCIDRのレンジで割り当てる。』

『Neutron portが作成されたとき、Kuryr remote driverはdocker daemonに、SUCCESSを示す空のレスポンス({})を生成する。([参考](https://github.com/docker/libnetwork/blob/master/docs/remote.md#create-endpoint))』

```json
{
    "Interface": {"MacAddress": "08:22:e0:a8:7d:db"}
}
```

『上記SUCCESSを受け取ると、libnetworkは以下のようなjsonリクエストを/NetworkDriver.JoinにHTTP POSTで発行する。』

```json
{
    "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
    "SandboxKey": "/var/run/docker/netns/052b9aa6e9cd",
    "Options": null,
    "EndpointID": "edb23d36d77336d780fe25cdb5cf0411e5edd91b0777982b4b28ad125e28a4dd"
}
```

『Kuryrは、以下の手順でコンテナを対応したNeutron networkに接続する。』

(1) 『vethペアを作成する』
(2) 『vethの片方をDockerが作ったコンテナのnetnsに設定する』
(3) 『Neutron Portのtype(binding:vif_type)に基づいたVIF Bindingを実施する』

『VIF Bindingの完了後、Kuryr remote network driverはDocker daemonに対して、[libnetworkのドキュメントに書いてある、joinリクエストとして](https://github.com/docker/libnetwork/blob/master/docs/remote.md#join)レスポンスを生成する』

ちなみにVIF Bindingは具体的にどう実行されるかというと、今の実装ではPortのbinding:vif_typeの値(ovsとかmidonetとか)を見て、対応するコマンドをキックすることで実施する。コマンドはデフォルトでは「/usr/libexec/kuryr/\<vif_typeの値\>」で、引数が「Neutron Portのid、 コンテナのvethのifname」。例えばvif_typeがmidonetであれば、/usr/libexec/kuryr/midonetというコマンドの中で、mm-ctl --bind-portを実行したりする。

３. 『ユーザがNetworkの情報を要求』

```bash
$ sudo docker network inspect foo
 {
     "name": "foo",
     "id": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
     "scope": "local",
     "driver": "kuryr",
     "ipam": {
         "driver": "default",
         "config": [
             {}
         ]
     },
     "containers": {
         "78c0458ba00f836f609113dd369b5769527f55bb62b5680d03aa1329eb416703": {
             "endpoint": "edb23d36d77336d780fe25cdb5cf0411e5edd91b0777982b4b28ad125e28a4dd",
             "mac_address": "02:42:c0:a8:7b:cb",
             "ipv4_address": "172.18.0.2/24",
             "ipv6_address": ""
         }
     }
 }
```

４. 『ユーザが別のコンテナをNetworkにつないでみる』

```bash
$ sudo docker network connect foo container2
 d7fcc280916a8b771d2375688b700b036519d92ba2989622627e641bdde6e646

$ sudo docker network inspect foo
 {
     "name": "foo",
     "id": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
     "scope": "local",
     "driver": "kuryr",
     "ipam": {
         "driver": "default",
         "config": [
             {}
         ]
     },
     "containers": {
         "78c0458ba00f836f609113dd369b5769527f55bb62b5680d03aa1329eb416703": {
             "endpoint": "edb23d36d77336d780fe25cdb5cf0411e5edd91b0777982b4b28ad125e28a4dd",
             "mac_address": "02:42:c0:a8:7b:cb",
             "ipv4_address": "172.18.0.2/24",
             "ipv6_address": ""
         },
         "d7fcc280916a8b771d2375688b700b036519d92ba2989622627e641bdde6e646": {
             "endpoint": "a55976bafaad19f2d455c4516fd3450d3c52d9996a98beb4696dc435a63417fc",
             "mac_address": "02:42:c0:a8:7b:cc",
             "ipv4_address": "172.18.0.3/24",
             "ipv6_address": ""
         }
     }
 }
```

５. 『ユーザがNetworkからコンテナを切断してみる』

```bash
$ CID=d7fcc280916a8b771d2375688b700b036519d92ba2989622627e641bdde6e646
$ sudo docker network disconnet foo $CID
```

『この操作では、/NetworkDriver.Leaveに以下のようなJSONがHTTP POSTで送られる。』

```json
{
    "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
    "EndpointID": "a55976bafaad19f2d455c4516fd3450d3c52d9996a98beb4696dc435a63417fc"
}
```

『Kuryr remote network driverはコンテナとNeutron portの間のVIF bindingを削除して、Docker daemonに空のレスポンスを返し、libmetworkは/NetworkDriver.DeleteEndpointに以下のようなJSONリクエストをHTTP POSTで送る。』

```json
{
    "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364",
    "EndpointID": "a55976bafaad19f2d455c4516fd3450d3c52d9996a98beb4696dc435a63417fc"
}
```

『Kuryr remote network driverは、関連するNeutron portを削除するNeutron APIのリクエストを生成し、この際関連したsubnetのportが空になる場合、Kuryrはsubnet自体も削除するようなリクエストも生成する。Neutron API実行後、Docker daemonに空のレスポンス({})を返す。』

６. 『ユーザが作ったfooというNetworkを削除する』

```bash
$ sudo  docker network rm foo
```

『この操作では、/NetworkDriver.DeleteNetworkに以下のようなJSONがHTTP POSTで送られる。』

```json
   {
       "NetworkID": "286eddb51ebca09339cb17aaec05e48ffe60659ced6f3fc41b020b0eb506d364"
   }
```

『Kuryr remote network driver は関連するNeutron Networkを削除するNeutron APIへのリクエストを生成する。Networkが削除されたら、Docker daemonに空のレスポンス({})を返す。』

# libnetworkとNeutronのモデルの対応

『何回か記述が出てきたけど、KuryrはNeutron clientを通してNeutronと通信し、libnetworkとNeutronの間のモデルを翻訳して橋渡しをする。』

『以下に、２つのモデルの間の対応を示す。』

|libnetwork|Neutron|
|:--|:--|
|Network|Network|
|Sandbox|Subnet, Port, netns|
|Endpoint|Port|

絵にすると多分こんな感じ。

![kuryr2.png](https://qiita-image-store.s3.amazonaws.com/0/45715/ff0e074d-1541-6d2b-58b9-5427ac2dab35.png)


ワークフローを見てもわかるけど、正式に1:1ではないので注意。

『libnetworkのSandboxとEndpointはNeutronのSubnetとPortにマッピングされるが、Sandboxはユーザからは直接は見えず、
Endpointがユーザから直接見え、かつコンテナにアタッチできる唯一のリソースになる。SandboxはEndpointによって裏で自動的に公表される情報を管理する。』