CoreOSのクラスタ環境（CoreOS+etcd+fleet）でdocker-composeを使う方法

#はじめに
この記事は[Docker2 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/docker2)の５日目の記事です。

このエントリではCoreOSのクラスタ環境（CoreOS+etcd+fleet）でdocker-composeを使う方法について書きます。
最近、Dockerをホスティングするための軽量OSとして、CoreOSを使うべく色々触っているのですが、標準で入っているfleetというオーケストレーションツールがシンプルでいい感じだと感じています。
しかし、同一ホストでコンテナ連携する機能が使いづらく（無いわけじゃない）感じたので、そこをdocker-compose入れてサンプルアプリケーションを立ち上げてみた際の内容になります。

ちなみに、少し古いですが、CoreOSに関する概要は以下に纏められており、参考になりました。
http://qiita.com/mopemope/items/fa9424b094aae3eac580

#### 使用ソフトウェアとバージョン
CoreOS：stable 1185.5.0
docker：1.11.2
docker-compose：1.9.0

#CoreOSクラスタを立ち上げる
まずはCoreOSのクラスタをAWS上に立ち上げます。
CoreOSが公式にCloudFormationのテンプレートを用意しているのでそれを利用します。
以下のURLにリンクが並んでいるので好きなリージョンでLaunchします。
https://coreos.com/os/docs/latest/booting-on-ec2.html

途中の設定で出てくるDiscoveryURLというパラメータについてですが、CoreOS（がクラスタリングに使っているetcd）のクラスタは初期構築の際、リーダ選出やメンバ発見などの情報をやり取りするDiscoveryURLを指定して起動する必要があります。
このDiscoveryURLを発行するサービスをCoreOS社が提供してくれているので、基本それを利用します。
DiscoveryURLは以下のURLで発行できます。末尾のsizeのパラメータはクラスタサイズに合わせます。今回は５台でクラスタを作成します。
https://discovery.etcd.io/new?size=5
アクセスするとURLが表示されるので、それをDiscoveryURLに設定します。

立ち上がったら、適当なインスタンスにログインして``fleetctl list-machines``を打って５台認識されていればOKです。

```shell-session:fleetctl
core@ip-172-31-28-248 ~ $ fleetctl list-machines
MACHINE		IP		METADATA
1f23b08a...	172.31.10.104	-
6738f2ad...	172.31.28.248	-
81e310a5...	172.31.10.105	-
91cdf348...	172.31.28.247	-
d639bc34...	172.31.10.103	-
```

# CoreOSクラスタにDocker Composeをインストールする
残念ながらCoreOSには標準でdocker-composeが入っていないのでインストールする必要があります。
しかし、CoreOSの思想は「アプリケーションはすべてコンテナで動かすべし」なのでCoreOSにはパッケージマネージャがありません。
CoreOSのSDKを使えば、追加のアプリケーションをインストールしたイメージを作ることができるのですが、わざわざ独自イメージを作成するのは少し敷居が高いです。
なので、githubのdownloadページにあるdocker-composeのバイナリを直接配置することにします。
https://github.com/docker/compose/releases

手動で入れるには以下のような手順になります。が、これを全台で展開するのは少々めんどくさいです。

```shell-session:install_docker-compose
core@ip-172-31-4-162 ~ $ sudo -i
ip-172-31-4-162 ~ # mkdir -p /opt/bin
ip-172-31-4-162 ~ # curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /opt/bin/docker-compose
ip-172-31-4-162 ~ # chmod +x /opt/bin/docker-compose
ip-172-31-4-162 ~ # exit
```


fleetctlというコマンドを使ってクラスタ全台に展開します。fleetはCoreOSに標準で入っているオーケストレーションツールで、fleetctlはそのCLIになります。fleetはCoreOSクラスタ全体に対するinitツールのような働きをします。
少し古いですが、fleetに関する概要は以下に纏められており、参考になりました。
http://deeeet.com/writing/2014/11/20/fleet/

fleetctlはsystemdのUnitファイルを一部拡張した記述形式のファイルを用いて、クラスタにサービスをデプロイします。docker-composeのinstallで使うUnitファイルは以下になります。

```bash:compose-install.service
[Unit]
Description=compose install Service
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=0

Environment=COMPOSE_VERSION="1.9.0"
ExecStartPre=-/usr/bin/sudo /usr/bin/sh -c "/usr/bin/mkdir -p /opt/bin"
ExecStartPre=-/usr/bin/sudo /usr/bin/sh -c "usr/bin/mkdir -p /opt/docker"
ExecStartPre=-/usr/bin/sudo /usr/bin/sh -c \
  "curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` \
  -o /opt/docker/docker-compose_${COMPOSE_VERSION}"
ExecStartPre=-/usr/bin/sudo /usr/bin/sh -c \
  "/usr/bin/chmod +x /opt/docker/docker-compose_${COMPOSE_VERSION}"
ExecStartPre=/usr/bin/sudo /usr/bin/sh -c \
  "/usr/bin/ln -f -s /opt/docker/docker-compose_${COMPOSE_VERSION} /opt/bin/docker-compose"
ExecStart=/opt/bin/docker-compose -v

[X-Fleet]
Global=true
```

UnitやServiceのセクションの記述はSystemdと同じです。
[Unit]セクションではサービス間の依存や実行の前後関係を記載します。
[Service]セクションではサービスが実行するコマンドを記載します。``ExecStartPre``は``ExecStart``の前に実行するコマンドを記載します。``=-``はコマンドが失敗しても次のコマンドに進むことを示しています。
[X-Fleet]のセクションはfleetで拡張された記述となっており、Global=trueはクラスタ全台にサービスを実行することを示しています。

上記のUnitファイルをコマンドを実行するCoreOS上に配置し、以下のコマンドでサービスの展開を行います。

```shell-session:install_docker-compose
core@ip-172-31-28-248 ~ $ fleetctl start compose-install.service
Unit compose-install.service 
Triggered global unit compose-install.service start
core@ip-172-31-28-248 ~ $ fleetctl list-units
UNIT			MACHINE				ACTIVE	SUB
compose-install.service	1f23b08a.../172.31.10.104	active	exited
compose-install.service	6738f2ad.../172.31.28.248	active	exited
compose-install.service	81e310a5.../172.31.10.105	active	exited
compose-install.service	91cdf348.../172.31.28.247	active	exited
compose-install.service	d639bc34.../172.31.10.103	active	exited
core@ip-172-31-28-248 ~ $ docker-compose -v
docker-compose version 1.9.0, build 2585387
```

うまく行けば、すべてのホストで一連のコマンドが実行され、actice/exitedの状態になります。

インストールを上記の形式でサービスとして展開すると、クラスタに新たノードが追加された時も、特に何もしなくても自動的にcompose-insttall.serviceが実行されて、docker-composeのバイナリが配置されます。
CloudFormationのClusterSizeを増やせばノードが追加できるので、確認ができますが、ここでは割愛します。

#Composeのサンプルプロジェクトを作成してgithubに上げる
クラスタにdocker-composeがインストールされたので、docker-composeで使うコンテナのDockerfileとdocker-compose.ymlを作成します。
サンプル用に[docker-composeのチュートリアルで使う、PythonとRedisのアプリケーション](https://docs.docker.com/compose/gettingstarted/)をデプロイすることにします。
アクセス回数をredisに保存してそれを表示するシンプルなアプリケーションです。詳細は上記のリンクを見てみて下さい。
以下のようにひとまとめにして、githubに上げます。

```bash:composetest
composetest
├── Dockerfile
├── README.md
├── app.py
├── docker-compose.yml
└── requirements.txt
```

一応docker-compose.ymlだけ中身を転記しておくと、こんな感じです。

```yaml:docker-compose.yml
version: '2'
services:
  web:
    build: .
    ports:
     - "5000:5000"
    volumes:
     - .:/code
    depends_on:
     - redis
  redis:
    image: redis
```

#Composeアプリをクラスタに展開する
上記のcomposetestアプリケーションをfleetctlでクラスタに展開します。
使用するUnitファイルは以下のような感じになります。

```bash:composetest@.service
[Unit]
Description=Sample docker-compose service
After=compose-install.service
Requires=compose-install.service

[Service]
TimeoutStartSec=0
ExecStartPre=-/opt/bin/docker-compose -f /home/core/git/composetest/docker-compose.yml kill
ExecStartPre=-/opt/bin/docker-compose -f /home/core/git/composetest/docker-compose.yml rm

ExecStartPre=-/usr/bin/mkdir /home/core/git
ExecStartPre=-/usr/bin/git clone https://github.com/kanga333/composetest.git /home/core/git/composetest
ExecStartPre=/usr/bin/git -C /home/core/git/composetest pull

ExecStartPre=/opt/bin/docker-compose -f /home/core/git/composetest/docker-compose.yml build

ExecStart=/opt/bin/docker-compose -f /home/core/git/composetest/docker-compose.yml up
ExecStop=/opt/bin/docker-compose -f /home/core/git/composetest/docker-compose.yml stop

[X-Fleet]
Conflicts=composetest@*.service
```

``ExecStartPre``で実行している内容は、同じdocker-composeアプリが何らか原因で残っていた場合に備えてのゴミ消し、gitレポジトリの最新化、docker-composeで使用するイメージのビルドになります。
ファイル名に@がついていますが、@をつけると、共通のUnitファイルから複数のinstanceを作成することができます。
例えば、以下のコマンドを実行すると``composetest@.service``のUnitファイルを使ったinstance1のサービスがCoreOSクラスタのいずれかのホストで起動されます。

```shell-session:start_instance1
core@ip-172-31-28-248 ~ $ fleetctl start composetest@1.service
Unit composetest@1.service inactive
Unit composetest@1.service launched on 1f23b08a.../172.31.10.104
```

同様に２台目、３台目は以下のようにして起動します。

```shell-session:start_instance2-3
core@ip-172-31-28-248 ~ $ fleetctl start composetest@2.service
Unit composetest@2.service inactive
Unit composetest@2.service launched on 6738f2ad.../172.31.28.248
core@ip-172-31-28-248 ~ $ fleetctl start composetest@3.service
Unit composetest@3.service inactive
Unit composetest@3.service launched on 81e310a5.../172.31.10.105
```

Unitファイルの[X-Fleet]セクションに記載してある``Conflicts=composetest@*.service``はすでにcomposetest@.serviceのインスタンスが起動しているホストに起動しない事を示しています。
つまり、3台のインスタンスを冗長化のために起動したけど、うっかり同じホストで動いていた、などといった事態を防ぐことができます。

起動後、少し時間を置いてから以下のコマンドを打つとサービスが起動していることが確認できます。

```shell-session:
core@ip-172-31-28-248 ~ $ fleetctl list-units | grep composetest
composetest@1.service	1f23b08a.../172.31.10.104	active	running
composetest@2.service	6738f2ad.../172.31.28.248	active	running
composetest@3.service	81e310a5.../172.31.10.105	active	running
```

curlで接続確認します。（※AWSのSGのポート5000番を開ける必要があります。）

```
core@ip-172-31-28-248 ~ $ curl 172.31.10.104:5000
Hello World! I have been seen 1 times.
```

接続確認が取れました。無事にfleet経由でdocker-composeのアプリケーションが起動しています。

#おわりに
以上でCoreOSのクラスタ環境にdocker-composeを入れて、可動確認を取ることができました。
とはいえ、立ち上がったね、はい終わり。では使い物にならないので、サービスディスカバリとか監視とか諸々を導入する必要があります。
ちょうど、来週のDocker2アドベントカレンダーが空いているので、その辺りをDockerコンテナで導入する話を書こうかと思います。

## 追記
Traefikでサービスディスカバリする方法について以下に書きました。
[CoreOS環境でtraefikを使ってコンテナのサービスディスカバリを行う方法](http://qiita.com/kanga/items/8fe2face1f8611e340ae)