Apolloを使ってAWSにMesos+Marathon+Chronos環境を構築する

# Apollo

[Apollo](http://capgemini.github.io/devops/apollo/)はMesosクラスタおよび関連のサービスをデプロイするためのツールです。

現在は、AWS, Digital Ocean, GCPに対応しています。OpenStackへの対応も予定されています。

関連のサービスとしては現在は[Chronos](https://mesos.github.io/chronos/docs/), [Marathon](http://mesosphere.github.io/marathon/)等に対応しています。将来的には[Kubernetes](http://kubernetes.io/)や[Spark](http://spark.apache.org/)にも対応していく計画のようです。

[Apollo/roadmap.md at master · Capgemini/Apollo](https://github.com/Capgemini/Apollo/blob/master/docs/roadmap.md)

様々なオープンソースのツールを使っているのが特徴です。以下のようなツールが使用されています。

* [Apache Mesos](http://mesos.apache.org/)
* [Docker](https://www.docker.com/)
* [Ansible](http://www.ansible.com/)
* [Terraform](https://terraform.io/)
* [Consul](https://www.consul.io/)
* [cAdvisor](https://github.com/google/cadvisor)
* [Weave](http://weave.works/)
* [HAProxy](http://www.haproxy.org/)

# AWSにMesosクラスタをデプロイする

AWSへのデプロイはVPCを使った方法と使わない方法の2種類が用意されていますが、今回は構成要素が少ないVPCを使わない方法で試してみます。

[Apollo/aws-public.md at master · Capgemini/Apollo](https://github.com/Capgemini/Apollo/blob/master/docs/getting-started-guides/aws-public.md)

## AWS, Atlasアカウントの用意

* AWSアカウント
    * AmazonEC2FullAccessポリシーを設定し、インスタンスやELBの操作ができるようにしておきます
* Atlasアカウント
    * TerraformからAtlasに登録されたArtifactを使用するためにアカウントを作成しトークンを取得しておきます
        * [Atlas by HashiCorp](https://atlas.hashicorp.com/)
        * AWSについてはTerraformの不具合のためAMIのIDをハードコーディングしています(2015/09/23時点)

## Terraform, Pythonのインストール

* Terraformは最新版をインストールしておきます
    * ドキュメントだと>=0.5.0になっていますが古いとバージョンチェックで弾かれる可能性があります
* Python >=2.7.5をインストールしておきます

## SSH鍵の作成

ドキュメントに従いSSH鍵を作成します。

[Apollo/aws-public.md at master · Capgemini/Apollo](https://github.com/Capgemini/Apollo/blob/master/docs/getting-started-guides/aws-public.md)

## レポジトリのクローン

```bash
$ git clone https://github.com/Capgemini/apollo.git
$ cd apollo
```

## Pythonパッケージのインストール

Ansibleなどクラスタ構築に使用されるPythonパッケージがインストールされます。

```bash
$ pip install -r requirements.txt
```

## 環境変数の設定

Apolloがクラスタ構築を行うための環境変数を設定します。
今回は以下の環境変数を設定します。AWS, Atlasの認証情報についてはお使いの環境にあわせて書き換えてください。

[direnv](http://direnv.net/)などを使うと何度もコマンドを実行しなくて済むので楽です。

```bash
export APOLLO_PROVIDER=aws-public
export TF_VAR_access_key=AWS_ACCESS_KEY
export TF_VAR_secret_key=AWS_SECRET_KEY
export TF_VAR_key_name="deployer"
export TF_VAR_key_file=AWS_SSH_KEY
export ATLAS_TOKEN=ATLAS_TOKEN
```

slaveの数、リージョン、インスタンスタイプ等も環境変数により設定することが可能です。
使用可能な環境変数については以下のファイルを参照してください。

```bash
bootstrap/aws-public/config-default.sh
```

## クラスタの構築・削除

以下のコマンドでクラスタが構築・起動されます。
Terraform・AnsibleでAWSのリソースが作成・設定されていく様子をみることができます。

完了するとブラウザでConsulやMarathon等のウィンドウが開きます。

```bash
$ /bin/bash bootstrap/apollo-launch.sh
```

Ansibleの設定等で失敗してしまう場合は、pipやAnsibleのバージョンをあげるとうまくいくかもしれません。

## クラスタの削除

クラスタが不要になったら以下のコマンドでクラスタが削除されます。

```bash
$ bash bootstrap/apollo-down.sh
```

# Mesosクラスタを使ってみる

## Ansible Dynamic Inventoryを使ってホストの情報を取得する

ApolloはTerraformで作成したインスタンスをAnsibleでデプロイする際に、Terraformで作成したインスタンスの情報を.tfstateファイルから[Dynamic Inventory](http://docs.ansible.com/ansible/intro_dynamic_inventory.html)を使って取得しています。

Dynamic Inventoryのスクリプトは[CiscoCloud/terraform.py](https://github.com/CiscoCloud/terraform.py)（を[フォークしたCapgemini/terraform.py](https://github.com/Capgemini/terraform.py)）を使用しています。

```inventory/terraform.py``` にファイルがあるので、次のようにホストの情報を取得することができます。

```bash
$ ./inventory/terraform.py --list
$ ./inventory/terraform.py --host apollo-mesos-master-0
```

## Anisbleを使った各ホスト上でのコマンド実行とロールによるホストのグルーピング

Dynamic Inventoryで取得したホスト情報を使って、各ホストでコマンドを実行することができます。

```bash
$ ansible -i inventory/terraform.py "*" -a hostname
apollo-mesos-slave-0 | success | rc=0 >>
ip-10-0-0-51

apollo-mesos-master-2 | success | rc=0 >>
ip-10-0-2-8

apollo-mesos-master-1 | success | rc=0 >>
ip-10-0-1-247

apollo-mesos-master-0 | success | rc=0 >>
ip-10-0-0-43
```

またロールごとにコマンドを実行することも可能です。
次のようにマスター、スレーブごとにコマンドを実行することができます。

```bash
$ ansible -i inventory mesos_masters -a hostname
apollo-mesos-master-1 | success | rc=0 >>
ip-10-0-1-247

apollo-mesos-master-0 | success | rc=0 >>
ip-10-0-0-43

apollo-mesos-master-2 | success | rc=0 >>
ip-10-0-2-8

$ ansible -i inventory mesos_slaves -a hostname
apollo-mesos-slave-0 | success | rc=0 >>
ip-10-0-0-51

```

ロールごとのコマンド実行を可能にするための設定は```inventory/inventory``` に書かれています。このファイルの先頭部分をみると次のようになっておりロールでホストをグループ分けしていることがわかります。（詳しい書き方は[ドキュメント](http://docs.ansible.com/ansible/intro_dynamic_inventory.html)を参照）

```ini:inventory/inventory 
[role=mesos_masters]

[role=mesos_slaves]

[mesos_masters:children]
role=mesos_masters

[mesos_slaves:children]
role=mesos_slaves

```

ロールはTerraformでaws_instanceを定義するときにtagで設定されており、terraform.pyがAnsibleのロールとして出力しています。

```text:terraform/aws-public/mesos-masters.tf
  tags = {                                                                         
    Name = "apollo-mesos-master-${count.index}"                                    
    role = "mesos_masters"                                                         
  }  
```

```text:terraform/aws-public/terraform.tfstate
                            "tags.#": "2",                                         
                            "tags.Name": "apollo-mesos-master-0",                  
                            "tags.role": "mesos_masters",
```

```bash
$ ./inventory/terraform.py --host apollo-mesos-master-0  | jq .role
"mesos_masters"
```

## 各ホストで起動しているDockerコンテナを調べる

ApolloはMesosクラスタのコンポーネントをホストに直接インストールするのではなくDockerコンテナで実行しています。

Ansibleで```docker ps```コマンドを実行することで、各ホストで起動しているコンテナを調べることができます。root権限が必要なので```-b``` オプションを付ける必要があります。

```bash
$ ansible -b -i inventory "*" -a "docker ps" 
apollo-mesos-master-2 | success | rc=0 >>
CONTAINER ID        IMAGE                                           COMMAND                  CREATED             STATUS              PORTS                    NAMES
142da24684f5        mesosphere/marathon:v0.10.1                     "./bin/start --artifa"   26 minutes ago      Up 26 minutes       0.0.0.0:8080->8080/tcp   marathon
cd390ecd4616        mesosphere/mesos-master:0.23.0-1.0.ubuntu1404   "mesos-master --regis"   26 minutes ago      Up 26 minutes                                mesos-master
2922dffe01d9        mesosphere/mesos:0.23.0-1.0.ubuntu1404          "/usr/share/zookeeper"   26 minutes ago      Up 25 minutes                                zookeeper
b8f0bbe70dd1        google/cadvisor:latest                          "/usr/bin/cadvisor"      27 minutes ago      Up 27 minutes       0.0.0.0:8081->8080/tcp   cadvisor
88300025f4a5        gliderlabs/registrator:master                   "/bin/registrator -in"   27 minutes ago      Up 27 minutes                                registrator
82658e458a68        andyshinn/dnsmasq                               "dnsmasq -k -r /etc/d"   27 minutes ago      Up 27 minutes                                dnsmasq

apollo-mesos-slave-0 | success | rc=0 >>
CONTAINER ID        IMAGE                                          COMMAND                  CREATED             STATUS              PORTS                    NAMES
07910110c741        asteris/haproxy-consul                         "/launch.sh"             24 minutes ago      Up 24 minutes                                haproxy
fb02b0131fd1        mesosphere/mesos-slave:0.23.0-1.0.ubuntu1404   "mesos-slave"            25 minutes ago      Up 25 minutes                                mesos-slave
c09d8e11694b        google/cadvisor:latest                         "/usr/bin/cadvisor"      27 minutes ago      Up 27 minutes       0.0.0.0:8081->8080/tcp   cadvisor
27d3f8363a00        gliderlabs/registrator:master                  "/bin/registrator -in"   27 minutes ago      Up 27 minutes                                registrator
a1ed902190ca        andyshinn/dnsmasq                              "dnsmasq -k -r /etc/d"   27 minutes ago      Up 27 minutes                                dnsmasq

apollo-mesos-master-0 | success | rc=0 >>
CONTAINER ID        IMAGE                                           COMMAND                  CREATED             STATUS              PORTS                    NAMES
29b10699aaeb        mesosphere/marathon:v0.10.1                     "./bin/start --artifa"   26 minutes ago      Up 26 minutes       0.0.0.0:8080->8080/tcp   marathon
a58baf126727        mesosphere/mesos-master:0.23.0-1.0.ubuntu1404   "mesos-master --regis"   26 minutes ago      Up 26 minutes                                mesos-master
61f62dc803c5        mesosphere/mesos:0.23.0-1.0.ubuntu1404          "/usr/share/zookeeper"   26 minutes ago      Up 25 minutes                                zookeeper
7f8d7bb4d946        google/cadvisor:latest                          "/usr/bin/cadvisor"      27 minutes ago      Up 27 minutes       0.0.0.0:8081->8080/tcp   cadvisor
e711621714e9        gliderlabs/registrator:master                   "/bin/registrator -in"   27 minutes ago      Up 27 minutes                                registrator
69c456497a81        andyshinn/dnsmasq                               "dnsmasq -k -r /etc/d"   27 minutes ago      Up 27 minutes                                dnsmasq

apollo-mesos-master-1 | success | rc=0 >>
CONTAINER ID        IMAGE                                           COMMAND                  CREATED             STATUS              PORTS                    NAMES
b5962e6a9bb4        mesosphere/marathon:v0.10.1                     "./bin/start --artifa"   26 minutes ago      Up 26 minutes       0.0.0.0:8080->8080/tcp   marathon
765e78175de9        mesosphere/mesos-master:0.23.0-1.0.ubuntu1404   "mesos-master --regis"   26 minutes ago      Up 26 minutes                                mesos-master
225f9ee1ff8f        mesosphere/mesos:0.23.0-1.0.ubuntu1404          "/usr/share/zookeeper"   26 minutes ago      Up 25 minutes                                zookeeper
c9a7ef7a0b0c        google/cadvisor:latest                          "/usr/bin/cadvisor"      27 minutes ago      Up 27 minutes       0.0.0.0:8081->8080/tcp   cadvisor
75844473c11b        gliderlabs/registrator:master                   "/bin/registrator -in"   27 minutes ago      Up 27 minutes                                registrator
c332d88a7194        andyshinn/dnsmasq                               "dnsmasq -k -r /etc/d"   27 minutes ago      Up 27 minutes                                dnsmasq
```

## mesos-executeを使ってコマンドを実行する

ここからはMesosphereのチュートリアルに従ってMesosを使ってみます。

[Advanced Mesos Course](https://open.mesosphere.com/advanced-course/)

まずは、[Exercise 3: Using Apache Mesos](https://open.mesosphere.com/advanced-course/using-apache-mesos/)で説明されている```mesos-execute``` コマンドを使ってコマンドを実行してみます。

まず```apollo-mesos-master-0``` にログインします。SSHに必要なIP、ユーザー名はterraform.pyで確認することができます。

```bash
$ ./inventory/terraform.py --host apollo-mesos-master-0 | jq ".ansible_ssh_user, .ansible_ssh_host, .ansible_ssh_port"
```

ホストにログインできたら```docker exec``` でmesos-masterコンテナに入ります。
コンテナに入るとプロンプトが```ubuntu@ip-10-0-0-43:~$``` から```root@ip-10-0-0-43:/#``` のように変わります。

```bash
$ sudo docker exec -it mesos-master /bin/bash
```

コンテナ内で次のコマンドを実行するとMesosクラスタでsleepコマンドが実行されます。

```bash
$ MASTER=$(mesos-resolve `cat /etc/mesos/zk` 2>/dev/null)
$ mesos-execute --master=$MASTER --name="cluster-test" --command="sleep 40"
```

ブラウザで```http://<apollo-mesos-master-0のIP>:5050``` を開くと、実行したコマンドが表示されていることを確認することができます。

![スクリーンショット 2015-09-23 15.32.57.png](https://qiita-image-store.s3.amazonaws.com/0/15114/9617d455-ec6f-5195-96f2-714c4306176f.png)

## Marathonを使ってコマンドを実行する

ブラウザで```http://<apollo-mesos-master-0のIP>:8080``` を開くとMarathonのGUIが開きます。

「+ New App」ボタンを押して、IDに「test」、Commandに「python -m SimpleHTTPServer」と入力し「+ Create」ボタンを押します。

![スクリーンショット 2015-09-23 19.59.43.png](https://qiita-image-store.s3.amazonaws.com/0/15114/13a75c1a-bf8b-e425-34bd-6bf6675031b8.png)

「/test」がRunningになっていることを確認します。

![スクリーンショット 2015-09-23 20.02.56.png](https://qiita-image-store.s3.amazonaws.com/0/15114/bbfefd23-8d81-f9a1-9ac8-bd4470bdb858.png)

MesosのGUIにも実行したコマンドが表示されています。

![スクリーンショット 2015-09-23 20.08.49.png](https://qiita-image-store.s3.amazonaws.com/0/15114/a02531ff-f383-935f-9096-3cf3a66b5b3c.png)

この画面の右のほうに表示されているIPを調べて```http://<表示されているIP>:8000``` をブラウザで開くと以下のような画面が開きWebサーバーが起動していることを確認することができます。

![スクリーンショット 2015-09-23 20.11.42.png](https://qiita-image-store.s3.amazonaws.com/0/15114/ced6e9e2-2f96-d077-5c42-b05e279840cd.png)

MarathonのGUIで「/test」を選択し、「Destroy App」を押していったんWebサーバーを削除します。

もう一度MaratonのGUIから「+ New App」を押して次のように入力します。
Commandに```$PORT``` 変数を使うことで、ポートを衝突させることなくスケールさせることができるようになります。

![スクリーンショット 2015-09-23 20.17.47.png](https://qiita-image-store.s3.amazonaws.com/0/15114/a468ac4d-a61a-cf95-352f-a005ae62e74d.png)

「/test」を選択し、「scale」ボタンを押して適当な数を指定するとアプリケーションがその数になることが確認できます。GUIのリンクをクリックすると実際にWebサーバーが起動していることが確認できます。

### うまくいかない場合

#### Waitingから進まない場合

ステータスが「Waiting」のまま変わらなくなってしまう場合があるようです。（特に起動直後の/chronosとか）

![スクリーンショット 2015-09-23 18.10.42.png](https://qiita-image-store.s3.amazonaws.com/0/15114/d9d072f2-ab37-edeb-2a7d-5a978fc248f3.png)

このときはmarathonコンテナを1つ再起動してみると処理が進むことがあります。
再起動が成功するのにしばらく待っても処理が進まない場合には別のホスト（```apollo-mesos-master-1```, ```apollo-mesos-master-2```）のmarathonコンテナを再起動してみます。

```bash
$ ansible -b -i inventory "apollo-mesos-master-0" -a "docker restart marathon"
```

marathonコンテナの再起動自体に失敗してしまう場合はdockerを再起動してみます。

```bash
$ ansible -b -i inventory "apollo-mesos-master-0" -m service -a "name=docker state=restarted"
```

状態が「Waiting」から「Deploying」や「Running」になれば成功です。

#### Deployingから進まない場合

「Deploying」から「Running」に進まない場合は、slaveへのコンテナのデプロイに失敗している場合があります。

MesosのUIからstderrを確認したり、適当なコンテナをデプロイして正常に動いているか確認します。

```bash
$ ansible -b -i inventory "apollo-mesos-slave-0" -a "docker run --rm hello-world"
```

うまく動いていないようであればdockerを再起動してみます。

```bash
$ ansible -b -i inventory "apollo-mesos-slave-0" -m service -a "name=docker state=restarted"
```

## Marathonを使ってDockerコンテナを動かす

MarathonからでDockerコンテナを動かすこともできます。

以下の内容でjsonファイルを作成します。

```json:outyet.json
{
    "id": "outyet",
        "cpus": 0.2,
        "mem": 20.0,
        "instances": 1,
        "container": {
            "type": "DOCKER",
            "docker": {
                "image": "goexample/outyet",
                "network": "BRIDGE",
                "portMappings": [
                { 
                    "containerPort": 8080,
                    "hostPort": 0,
                    "servicePort": 0,
                    "protocol": "tcp"
                }
                ]
            }
        }
}
```

MarathonのAPIを使ってAppを作成します。

```bash
$ curl -X POST -HContent-Type:application/json -d @outyet.json http://<MarathonのIP>:8080/v2/apps
```

![スクリーンショット 2015-09-27 11.18.19.png](https://qiita-image-store.s3.amazonaws.com/0/15114/63ab7006-7e05-07ab-0c6f-1005740f1101.png)

ステータスがRunningにならない場合は、[Marathonからのコマンド実行がうまくいかない場合](#うまくいかない場合) と同様の対処を試してみます。

Runningになっていれば```/outyet``` をクリックして、表示されるリンク先にとぶと下図のような画面が表示されます。

![スクリーンショット 2015-09-27 11.22.06.png](https://qiita-image-store.s3.amazonaws.com/0/15114/b3891e14-5df4-0087-e6c1-f0195cfeebf4.png)

コマンドを実行した場合と同様に「scale」で数を増減させたりすることが可能です。

## Chronosを使って定期的にコマンドを実行する

MarathonのGUIから「/chronos」のリンクを開き、表示されているIPアドレスのリンクをクリックすると下図のようなChronosのGUIが開きます。

![スクリーンショット 2015-09-23 20.41.24.png](https://qiita-image-store.s3.amazonaws.com/0/15114/3f266262-4275-2fd1-30de-e900da990718.png)

Chronosを使うとMesosクラスタ上で定期的にコマンドを実行することができます。

「+ New Job」を押してNAME, COMMAND, SCHEDULEを次のように入力し、1分ごとに日時を出力するジョブを作成します。

![スクリーンショット 2015-09-23 20.43.00.png](https://qiita-image-store.s3.amazonaws.com/0/15114/2f19ca36-8e54-7eed-50cd-1e3da0436f7f.png)

「Create」を押すとジョブが作成されます。

![スクリーンショット 2015-09-23 20.45.13.png](https://qiita-image-store.s3.amazonaws.com/0/15114/7b3ae378-175c-7c77-6c8c-a0c948579c1b.png)

しばらくするとLASTがSUCCESSになります。

![スクリーンショット 2015-09-23 20.46.27.png](https://qiita-image-store.s3.amazonaws.com/0/15114/ee348142-86be-a096-4967-547059a85adf.png)

MesosのGUIを確認みるとCompleted TasksにChronosにより実行されたタスクが表示されています。

![スクリーンショット 2015-09-23 20.47.59.png](https://qiita-image-store.s3.amazonaws.com/0/15114/02d90484-89cc-c165-ceda-9b545d9a80f1.png)

各タスクのSandbox>stdoutを確認するとコマンドの出力を確認することができます。

```text
Registered executor on xxx.xxx.xxx.xxx
Starting task ct:1443008927000:0:date:
Forked command at 392
sh -c 'date'
Wed Sep 23 11:48:49 UTC 2015
Command exited with status 0 (pid: 392)
```

Chronosはあるジョブを他のジョブに依存して実行させることができます。

新規に先ほど作成した「date」に依存する「hello」というジョブを作成してみます。

![スクリーンショット 2015-09-23 22.46.57.png](https://qiita-image-store.s3.amazonaws.com/0/15114/bf50b531-b6d5-c24d-48c8-24657638bfc7.png)

より詳細なChronosの使い方は[ドキュメント](https://mesos.github.io/chronos/docs/)を参照してください。

トップで「Graph」ボタンを押すと依存関係のグラフを表示することができます。

![スクリーンショット 2015-09-23 22.50.54.png](https://qiita-image-store.s3.amazonaws.com/0/15114/0ddf889c-c03d-a619-a19e-3065c63fb025.png)

MesosのUIをみると1分ごとに「date」が実行され、直後に「hello」が実行されている様子がわかります。

![スクリーンショット 2015-09-23 22.51.58.png](https://qiita-image-store.s3.amazonaws.com/0/15114/e5686914-dfd6-6c05-f52b-3beedcda68fb.png)

# 最後に

Mesosや関連ツールについてほとんど知識がなかったのですが、構築自体は割とスムーズにできました。使いたいツール群をまとめて試せるのはありがたいです。

構築にはそれほど苦労しませんでしたが、コマンド実行が失敗するなど使い方にはつまずくところが結構ありました。特にDockerコンテナの実行はかなり失敗する確率が高かったです。各ツールについて理解を含めて対応できるようにしていきたいです。Dockerコンテナを複数ホストで動かすだけなら、構築もコマンドの実行もDocker Swarmの方がずっと簡単だと思います。

# 参考

* [瞬殺で作るMesos + Chronos + Marathon + Dockerクラスタ環境 - Qiita](http://qiita.com/TsuyoshiUshio@github/items/9d8f5b952b635d94ae6f)
* [Advanced Mesos Course](https://open.mesosphere.com/advanced-course/)