Docker on Mesos with OpenVNet 手順詳細

概要
----

Mesosでコンテナを起動する際にinterfaceを制御したり仮想ネットワークに所属させたりする手順について、MesosやMarathonのビルドとあわせて記載。

前提環境
--------

デモ環境は以下のホストで構成されていて、OSは全てCentOS6.6で行った。今回は冗長化は目的にしていないので、zookeeperとmasterを同居させて1台だけ存在する形にしている。

* mesos-master、zookeeper、marathon、vnmgr、webapi
* (mesos-slave、vna) × N

考え方
------

MesosでDockerコンテナをシステムコンテナのようなものとして扱う際に、interfaceを自由に設定し、仮想ネットワークを自動的に組むことが出来ないかを考える。仮想化ツールとしてはOpenVNetを利用する。

OpenVNetがネットワーク供給先の個体を認識するのは、

* その個体につながっているinterface（vethであればスイッチポートに近い方）は何か
* そのIP/netmask or prefixは何か
* そのMACアドレスは何か

であり、この情報を作るのはコンテナの外であるため、Mesosでコンテナを作っている箇所付近に上記情報でOpenVNetのAPIを実行するロジックを挟む形にした。

パラメータとしてAPIで受け取る必要がある情報は、

* 出来上がるコンテナのinterface（仮想ネットワークであればコンテナに近い方）は何か
* 出来上がるコンテナのinterfaceが所属すべき仮想ネットワークは何か
* そのIP/netmask or prefixは何か
* 他（今回はやらないが、RoutingTable情報など）

があるため、これをmesosとmarathonに追加している。

どんな仮想ネットワークを作るのか、などをOpenVNet側の仕事とし、コンテナを作り、そのinterfaceをどのように設定し、どの仮想ネットワークに所属させるか、をMesos側に寄せている。

なお、今回は情報管理はしないことにする。つまり、誰がNetworkDatabaseを持つべきかまでは考えてない。

各サーバのインストール
--------------------

### mesosのインストール

```bash:mesosへのパッチ当てとインストール
$ # 参考：http://mesos.apache.org/gettingstarted/
$ cat > /etc/yum.repos.d/wandisco-svn.repo <<EOF
[WandiscoSVN]
name=Wandisco SVN Repo
baseurl=http://opensource.wandisco.com/centos/6/svn-1.8/RPMS/\$basearch/
enabled=1
gpgcheck=0
EOF

$ sudo yum -y install autoconf make gcc gcc-c++ patch python-devel git libtool java-1.7.0-openjdk-devel zlib-devel libcurl-devel openssl-devel cyrus-sasl-devel apr-devel apr-util-devel cyrus-sasl-md5 subversion-devel 

$ wget http://mirror.nexcess.net/apache/maven/maven-3/3.0.5/binaries/apache-maven-3.0.5-bin.tar.gz
$ tar -zxf apache-maven-3.0.5-bin.tar.gz -C /opt/
$ ln -s /opt/apache-maven-3.0.5/bin/mvn /usr/bin/mvn
$ alternatives --config java

$ git clone https://github.com/qb0C80aE/Mesos_OpenVNet_Integration_Patch.git
$ git clone https://github.com/apache/mesos.git
$ cd mesos
$ git checkout d55a7239cf0707ec291d73e30b69a07f7ee845d3
$ export JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75.x86_64
$ ./bootstrap
$ ./configure --with-webui --with-included-zookeeper --disable-perftools --enable-frame-pointers
$ patch -p1 < ../Mesos_OpenVNet_Integration_Patch/mesos.patch
$ make
$ sudo make install
```

この後のmarathonのビルドでmesosのjarが必要になるが、mesosはmakeしただけだとjarをmvnリポジトリには配置しないようなので、mesosをmvn installしてから、mvnリポジトリ情報をmarathonのbuild.scalaに追加する（設定値についてはmarathon.patchを参照）。

```
$ cd /path/to/mesos
$ mvn install -f src/java/mesos.pom
```

### marathonのインストール

mesos自体はmakeの過程でprotocが実行されるため、特にprotocの明示実行は必要ないが、marathonのビルド時は実行されないようなので、自分で実行する必要がある。protocはダウンロードしてくるか、mesosの3rd-partyに入ってるものを使う。

また、パッチを適用すると（当然ながら）marathonのテストが軒並み動作しなくなるので、動作を優先にしてとりあえずテストはスキップするか削除しておく。

```marathonへのパッチ当てとインストール
$ sudo yum -y install http://rpm.typesafe.com/typesafe-repo-2.0.0-1.noarch.rpm
$ sudo yum -y install sbt
$ sbt
$ wget http://dl.bintray.com/sbt/native-packages/sbt/0.13.5/sbt-0.13.5.zip
$ unzip sbt-0.13.5.zip
$ mkdir -p ~/.sbt/.lib/0.13.5/
$ mv sbt/bin/sbt-launch.jar ~/.sbt/.lib/0.13.5/

$ git clone https://github.com/qb0C80aE/Mesos_OpenVNet_Integration_Patch.git
$ git clone https://github.com/mesosphere/marathon.git
$ cd marathon
$ git checkout 97663e1da58cb1d00380f36ce8f199d07aeb8318
$ sudo ln -s /path/to/mesos/3rdparty/libprocess/3rdparty/protobuf-2.5.0/src/protoc /usr/local/bin/protoc
$ protoc --java_out=src/main/java/ --proto_path=/path/to/mesos/include/ --proto_path=src/main/proto/ src/main/proto/marathon.proto
$ patch -p1 < ../Mesos_OpenVNet_Integration_Patch/marathon.patch
$ sbt assembly
```

### docker、netns対応iprouteのインストール

epelやrdoから各slaveホストにdockerとnetns対応のiprouteをインストールしておく。

### OpenVNetのインストールと設定

[ここ](http://qiita.com/qb0c80aE/items/8d176bdf4d2460849ed9)を参考に。mesos slaveのあるホストにvnaがいる形で設定する。

### virtual-network-driverの配置

mesosから実行される、コンテナのネットワークを設定するドライバを各mesos slaveホストに配置する。dp-node1などの値はslaveホストに応じて適宜変更する。

```bash:/opt/mesos/bin/virtual-network-driver配置
$ git clone https://github.com/qb0C80aE/Mesos_OpenVNet_Integration_Patch.git
$ mkdir -p /opt/mesos/bin
$ mv Mesos_OpenVNet_Integration_Patch/virtual-network-driver /opt/mesos/bin/
```

各種サーバの起動
-----------------

marathonの起動にはzookeeper連携が必須のため、zookeeperを起動する。

```bash:zookeeper
$ cd /path/to/mesos/3rdparty/zookeeper-3.4.5
$ cp ./conf/zoo_sample.cfg ./conf/zoo.cfg
$ cd bin
$ ./zkServer.sh start
```

masterに使うホストで、mesos-masterをzookeeper連携で起動。先述のとおり、今回は冗長化は目的にしていないので、quorumは1で起動する。

```bash:mesos-master
$ mesos-master --work_dir=/var/lib/mesos --quorum=1 --zk=zk://127.0.0.1:2181/mesos
```

slaveに使うホストで、mesos-slaveをzookeeper連携で起動。Task実行時にDocker Containerizerが利用できるように、--containerizers=dockerを指定する。

```bash:mesos-slave
$ mesos-slave --master=zk://＜masterのIP＞:2181/mesos --containerizers=docker --executor_registration_timeout=5mins
```

この時点で http://＜masterのIP＞:5050/ にアクセス可能になる。アクセスすると、slaveホストの分だけslaveが存在する状態になっている。

続いて、marathonを起動する。
※余談だが、marathonやchronosはScallopを利用しており、--http_portでlistenポートを指定できる。ポートがバッティングして困る場合はこのオプションを利用する。

```bash:marathon
$ cd /path/to/marathon/bin
$ ./start --master zk://＜masterのIP＞:2181/mesos --zk zk://＜masterのIP＞:2181/marathon
```

最後に、[ここ](http://qiita.com/qb0c80aE/items/8bdd22d104b06657fd89)およびパッチ内のvnet-init.shを参考に、OpenVNetを起動してnw-vnet1、nw-vnet2、nw-vnet3の仮想ネットワークを設定する。

```bash:vnet-init.sh
#!/bin/sh

set -e

mysqladmin -f -uroot drop vnet
mysqladmin -uroot create vnet
(cd /opt/axsh/openvnet/vnet; bundle exec rake db:init)

export vnmgr_host=127.0.0.1
export vnmgr_port=9090

# datapaths
curl -X POST \
--data-urlencode uuid=dp-node1 \
--data-urlencode display_name="node1" \
--data-urlencode dpid="0x0000000000000001" \
--data-urlencode node_id="node1" \
http://${vnmgr_host}:${vnmgr_port}/api/datapaths

# networks
curl -X POST \
--data-urlencode uuid=nw-vnet1 \
--data-urlencode display_name="nw-vnet1" \
--data-urlencode ipv4_network="10.0.0.0" \
--data-urlencode ipv4_prefix="24" \
--data-urlencode network_mode="virtual" \
http://${vnmgr_host}:${vnmgr_port}/api/networks

curl -X POST \
--data-urlencode uuid=nw-vnet2 \
--data-urlencode display_name="nw-vnet2" \
--data-urlencode ipv4_network="10.0.0.0" \
--data-urlencode ipv4_prefix="24" \
--data-urlencode network_mode="virtual" \
http://${vnmgr_host}:${vnmgr_port}/api/networks

curl -X POST \
--data-urlencode uuid=nw-vnet3 \
--data-urlencode display_name="nw-vnet3" \
--data-urlencode ipv4_network="192.168.0.0" \
--data-urlencode ipv4_prefix="24" \
--data-urlencode network_mode="virtual" \
http://${vnmgr_host}:${vnmgr_port}/api/networks

# dhcp if
curl -X POST \
--data-urlencode uuid="if-dhcp1" \
--data-urlencode owner_datapath_uuid="dp-node1" \
--data-urlencode mac_address="02:01:00:00:01:01" \
--data-urlencode network_uuid="nw-vnet1" \
--data-urlencode ipv4_address="10.0.0.2" \
--data-urlencode mode="simulated" \
http://${vnmgr_host}:${vnmgr_port}/api/interfaces

curl -X POST \
--data-urlencode uuid="if-dhcp2" \
--data-urlencode owner_datapath_uuid="dp-node1" \
--data-urlencode mac_address="02:02:00:00:01:01" \
--data-urlencode network_uuid="nw-vnet2" \
--data-urlencode ipv4_address="10.0.0.2" \
--data-urlencode mode="simulated" \
http://${vnmgr_host}:${vnmgr_port}/api/interfaces

curl -X POST \
--data-urlencode uuid="if-dhcp3" \
--data-urlencode owner_datapath_uuid="dp-node1" \
--data-urlencode mac_address="02:03:00:00:01:01" \
--data-urlencode network_uuid="nw-vnet3" \
--data-urlencode ipv4_address="192.168.0.2" \
--data-urlencode mode="simulated" \
http://${vnmgr_host}:${vnmgr_port}/api/interfaces

# dhcp ns
curl -s -X POST \
--data-urlencode uuid="ns-dhcp1" \
--data-urlencode interface_uuid="if-dhcp1" \
--data-urlencode type="dhcp" \
http://${vnmgr_host}:${vnmgr_port}/api/network_services

curl -s -X POST \
--data-urlencode uuid="ns-dhcp2" \
--data-urlencode interface_uuid="if-dhcp2" \
--data-urlencode type="dhcp" \
http://${vnmgr_host}:${vnmgr_port}/api/network_services

curl -s -X POST \
--data-urlencode uuid="ns-dhcp3" \
--data-urlencode interface_uuid="if-dhcp3" \
--data-urlencode type="dhcp" \
http://${vnmgr_host}:${vnmgr_port}/api/network_services

# dhcp lease policy
curl -s -X POST \
--data-urlencode uuid="lp-1" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies

curl -s -X POST \
--data-urlencode uuid="iprg-1" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups

curl -s -X POST \
--data-urlencode begin_ipv4_address="10.0.0.100" \
--data-urlencode end_ipv4_address="10.0.0.200" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups/iprg-1/ip_ranges

curl -s -X POST \
--data-urlencode ip_range_group_uuid="iprg-1" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies/lp-1/networks/nw-vnet1

curl -s -X POST \
--data-urlencode uuid="lp-2" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies

curl -s -X POST \
--data-urlencode uuid="iprg-2" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups

curl -s -X POST \
--data-urlencode begin_ipv4_address="10.0.0.100" \
--data-urlencode end_ipv4_address="10.0.0.200" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups/iprg-2/ip_ranges

curl -s -X POST \
--data-urlencode ip_range_group_uuid="iprg-2" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies/lp-2/networks/nw-vnet2

curl -s -X POST \
--data-urlencode uuid="lp-3" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies

curl -s -X POST \
--data-urlencode uuid="iprg-3" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups

curl -s -X POST \
--data-urlencode begin_ipv4_address="192.168.0.100" \
--data-urlencode end_ipv4_address="192.168.0.200" \
http://${vnmgr_host}:${vnmgr_port}/api/ip_range_groups/iprg-3/ip_ranges

curl -s -X POST \
--data-urlencode ip_range_group_uuid="iprg-3" \
http://${vnmgr_host}:${vnmgr_port}/api/lease_policies/lp-3/networks/nw-vnet3
```

動作確認
--------

marathonへのリクエスト（json）を準備する。

```json:marathon_request.json
{
  "container": {
    "type": "DOCKER",
    "docker": {
      "image": "centos:centos6"
    },
    "networkInterfaces": [
      {
        "networkName": "nw-vnet1",
        "interfaceName": "eth0",
        "bootproto": "dhcp",
        "ipAddress": "",
        "netmask": "",
        "gateway": ""
      },
      {
        "networkName": "nw-vnet3",
        "interfaceName": "eth1",
        "bootproto": "static",
        "ipAddress": "192.168.0.100",
        "netmask": "255.255.255.0",
        "gateway": "192.168.0.1"
      }
    ]
  },
  "id": "vnettest",
  "instances": 1,
  "cpus": 0.1,
  "mem": 128,
  "uris": [],
  "cmd": "/sbin/init"
}
```

準備が終わったら、marathonにリクエストを投入する。

```bash
$ curl -X POST -H "Content-Type: application/json" http://＜masterのIP＞:8080/v2/apps -d@marathon_request.json
```

dockerコンテナが1つ起動するので、docker execでコンテナ内のinterfaceを確かめる。
正しくinterfaceが起動してIPアドレスが振られていることを確認したら、marathonのscaleボタンでスケールアウトしてみる。


その他参考情報
--------

* Mesosのexecutorとtaskの関係

http://mesos.apache.org/documentation/latest/mesos-architecture/

* containerizer

https://github.com/apache/mesos/blob/master/docs/mesos-containerizer.md

docker containerizerはdockerコンテナでタスクを隔離する。mesos containerizerはもうちょっと簡易なもので隔離環境を作るが、ネットワークは隔離しない。その為、リクエストのcontainer typeにMESOSを指定しても、ネットワーク情報は利用できない形になる。

* CentOS6.Xにおける、コンテナ内でのinterface起動停止

以前[ここのコメント部分](http://qiita.com/qb0c80aE/items/8bdd22d104b06657fd89)に記載したように、ホスト側に同名のinterfaceがないと操作できないため、微妙だが、あらかじめダミーのtapなどを作っておく。

注意（所感）
----------

プレゼン資料にあるとおり、このデモではDockerコンテナをSystem Containerのような感じ（shutdown等は動作しない気がする）で扱っている。

元がPaaSなので当然といえば当然だが、Dockerはapplication centricであり、そもそも軽量仮想マシンを目指していない（rktも同様）。Supervisordなどの存在もあり、コンテナ内で複数プロセスを稼働させることはできるが、軽量仮想マシンはどちらかというとlxcやOpenVZの話であり、Dockeは基本的にApplication Containerを扱うもの。

Application Container PlatformにSDN関係を適用するという話は、要するにPaaSにSDN関係を適用すると言っているのと殆ど同じであり、IaaS上にシステム作るのと違って、PaaS上に構築したアプリケーションに個別に適用できるようなものでもなく、"PaaSを作る"際にPaaS側に機能として混ざってる類のもの。

そのため、このデモで対象にすべきは、本来はlxcやOpenVZが適切。