Machine/Compose/Swarmのまとめ(2015年5月版)

## 追記

* 2016年2月: 以下は2015年5月時点の情報です。いずれ更新または新規投稿するかもしれませんが、本記事の情報(概念的な部分はともかく特にデモ)は古い可能性がありますのでご了承ください。
* 2016年8月: [Docker1.12のSwarm modeの記事](http://qiita.com/kiida/items/6dac109dcd063609b064)を書きました

## はじめに

先日ちょうどDockerのオフィシャルな[オンラインMeetup](http://www.meetup.com/Docker-Online-Meetup/)でMachine/Compose/Swarmの話をしていたので、今更ながらキャッチアップついでにまとめてみました。
初心者向けに概要を掴む程度の内容の投稿です。

なお、日本時間だと深夜1時でしたので参加された方は少ないかもしれませんが動画が公開されています。

* [Machine](https://www.youtube.com/watch?v=xwj44dAvdYo)
* [Swarm](https://www.youtube.com/watch?v=UmSRB4H8JXU)
* [Compose](https://www.youtube.com/watch?v=5b2UqNzL8-Y)

DockerのMeetup自体は毎週行われており、Docker社の[ブログ](https://blog.docker.com/)にまとめが載るのでチェックしておくと良いかもしれません。
次回(5/19)はRegistry2.0の話です。

ということでそろそろ本題です。

## Machine

[Docker Machine](https://github.com/docker/machine)とはDockerがインストールされたホストマシンを作成するものです。
作成先はローカルのVM、クラウドサービスともに可能です。

![machine1.png](https://qiita-image-store.s3.amazonaws.com/0/36003/64bf6067-a62e-8fa9-9ea1-8cf1d1df5f11.png)

現時点では以下のプロバイダに対応しています。

* aws-ec2
* digitalocean
* azure
* gce
* openstack
* rackspace
* softlayer
* virtualbox
* vmware

既に大量にサポートされていますね。
ただ、DigitalOceanで試した様子ではまだまだ挙動が怪しくて、Ubuntu-14-04-x64以外はちゃんと動きませんでした。(fedora21、Ubuntu15.04、coreos-stableを試しました)
また、Ubuntu-14-04-x64でもオプションでプライベートネットワーキングを有効にしようとしたらDockerサービスが正常に立ち上がらない状態でVMが出来上がってしまいました。

まだベータ版なので、この辺りは仕方無いですね。
なお、2015年5月時点ではバージョン0.2が最新です。

## Compose

[Docker Compose](https://github.com/docker/compose)とはyml形式の設定ファイルに従って複数のDockerコンテナで構成される環境を構築したり操作したりするツールです。
以前はFigの名称で知られていました。

![compose1.png](https://qiita-image-store.s3.amazonaws.com/0/36003/8750b772-6cb9-933d-31ca-3db1fa28cc8c.png)


通常はローカルにDockerコンテナを立てますが、Machine/Swarmと組み合わせることでマルチノード上にDockerコンテナを立ち上げることも可能です。
また、例えば開発環境/検証環境/本番環境があった場合に、共通部分をcommon.yml等に共通化しておき、それぞれの環境用のファイル(production.yml等)でextendsするようなこともできます。([参考](http://docs.docker.com/compose/extends/))

なお、2015年5月時点ではバージョン1.2が最新です。

## Swarm

[Docker Swarm](https://github.com/docker/swarm)とはDockerのクラスタリングをサポートするツールです。
複数のDockerコンテナを1台のクライアントからdockerコマンドを使って一元管理できるようになります。

![swarm.png](https://qiita-image-store.s3.amazonaws.com/0/36003/682306ee-083a-8089-897d-3ebd0b739b44.png)


前述のMeetupの動画によると現状のDockerAPIのカバー率は85%程度だそうです。0.3では100%を目指すとのことです。

dockerコマンドでの操作では様々なフィルターを追加できます。
例えば以下のようなものがあります。

* `-m 2g` - 2GBのメモリを確保する
* `-e constraint:storage==ssd` - ssdというラベルが貼られたノード(Dockerホスト)上にDockerのインスタンスを立てる
* `-e constraint:region!=us*` - us以外のリージョンにDockerのインスタンスを立てる
* `-e affinity:image==redis` - redisと同じノード上にDockerのインスタンスを立てる

これらのフィルターはデフォルトではhard(条件に合わなければ実行できない)に適用されますが、`~==`または`~!=`を使うことでsoft(可能な場合は条件を満たすが不可能な場合はフィルターを捨てて実行する)に変更することもできます。

フィルターの詳細な説明は[こちら](https://github.com/docker/swarm/tree/master/scheduler/filter)にあります。
現状では残念ながらSwarmで使うラベル(上記の例だとどのノードがSSDかとか、どのUSリージョンか等)をMachineで設定することはできませんが、次バージョンでは[できるようになるようです](https://github.com/docker/machine/issues/974)。

なお、2015年5月時点ではバージョン0.2が最新です。

## 全部組み合わせて使ってみる

今回はDigitalOcean上にノードを立ててみようと思います。他のクラウドサービスを使っている方は適宜読み替えて頂ければと思います。
これを機にDigitalOceanを使いはじめるという方は[こちら](https://www.digitalocean.com/?refcode=7be3336a0400)から申し込んで貰えるとお得(多分お互い)のはずです。

なお、コマンドは全てローカルで実行します。

### インストール

まずはdocker(engine)をインストールします。

```bash:dockerのインストール
# 最新版のインストール
wget -qO- https://get.docker.com/ | sh

# ユーザにdockerコマンド実行可能にするためdockerグループに追加。
sudo usermod -aG docker <username>
```

次にMachineをインストールします。

```bash:docker-machineのインストール
sudo wget https://github.com/docker/machine/releases/download/v0.2.0/docker-machine_linux-amd64 -O /usr/local/bin/docker-machine
sudo chmod +x /usr/local/bin/docker-machine
```

次はComposeをインストールします。

```bash:docker-composeとbash用補完のインストール
sudo apt-get install python-pip
sudo pip install docker-compose
sudo wget https://raw.githubusercontent.com/docker/compose/1.2.0/contrib/completion/bash/docker-compose -O /etc/bash_completion.d/docker-compose
```

Swarmは明示的にインストールする必要はありません。

### Machine+SwarmでSwarmのクラスタを構築

まずはSwarmで使うトークンを作成します。

```bash:Swarmのトークンを作成
docker run --rm swarm create
565537d2ba368b2788445e6f583eefda
```

ここで返された値(565537d2ba368b2788445e6f583eefda)がswarmで使うトークンになります。
swarmのトークン以外にも、etcd/consul/Zookeeper等も使えるようです。

machineのコマンドでこのトークンを使ってdigitalocean上にSwarmのマスタノードを作ります。
今回はメモリ2GBのdroplet(digitaloceanのVMの呼び方)にしてみました。

```bash:swarm-masterの作成
docker-machine create -d digitalocean --digitalocean-image "ubuntu-14-04-x64" --digitalocean-region "sfo1" --digitalocean-size "2gb" --digitalocean-access-token ${DIGITALOCEAN_API_TOKEN} --swarm --swarm-master --swarm-discovery token://565537d2ba368b2788445e6f583eefda swarm-master
```

マスターができたら2台のノードを追加してみます。

```bash:swarm-nodeの作成
# swarm-node1
docker-machine create -d digitalocean --digitalocean-image "ubuntu-14-04-x64" --digitalocean-region "sfo1" --digitalocean-size "2gb" --digitalocean-access-token ${DIGITALOCEAN_API_TOKEN} --swarm --swarm-discovery token://565537d2ba368b2788445e6f583eefda swarm-node1

# swarm-node2
docker-machine create -d digitalocean --digitalocean-image "ubuntu-14-04-x64" --digitalocean-region "sfo1" --digitalocean-size "2gb" --digitalocean-access-token ${DIGITALOCEAN_API_TOKEN} --swarm --swarm-discovery token://565537d2ba368b2788445e6f583eefda swarm-node2
```

確認します。

```bash:確認
$ docker-machine ls
NAME           ACTIVE   DRIVER         STATE     URL                          SWARM
swarm-master            digitalocean   Running   tcp://192.241.228.14:2376    swarm-master (master)
swarm-node1             digitalocean   Running   tcp://192.241.233.78:2376    swarm-master
swarm-node2    *        digitalocean   Running   tcp://192.241.211.126:2376   swarm-master
```

無事に3台のマシンが出来上がりました。

### swarmでコンテナを作ってみる

Composeも連携させてみる前に一旦Swarm単体で操作してみます。

まず以下のコマンドを実行すると、今後のdockerコマンドは全てswarm-master経由で実行されるようになります。

```bash:
eval $(docker-machine env --swarm swarm-master)
```

eval付けないで実行するとこんな感じです。(実行結果のコメントは割愛)

```bash:実行結果
$ docker-machine env --swarm swarm-master
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="/home/dk/.docker/machine/machines/swarm-master"
export DOCKER_HOST=tcp://192.241.228.14:3376
```

つまり上記を`eval`つけて実行すれば`DOCKER_HOST`に`tcp://192.241.228.14:3376`が設定され、デフォルトで使うデーモンソケット(`/var/run/docker.sock`)ではなく`tcp://192.241.228.14:3376`のswarm-masterと通信するようになるってことですね。


次に以下のようにコンテナを作ってみます。
今回は特に連携等は考えずにredisとnginxのコンテナを作ってみます。

```bash:swarmで
# メモリ2GB割り当てるredisのコンテナを作成
docker run -p "6879:6879" -d --name redis -m 2g redis

# メモリ2GB割り当てるnginxのコンテナを2つ作成。作成するノードはredisとは別のノードにする。
docker run -p "80" -d --name web1 -e "affinity:container!=redis" -m 2g nginx
docker run -p "80" -d --name web2 -e "affinity:container!=redis" -m 2g nginx
```

そして`docker ps`で結果を確認すると以下のようになっています。

```bash:作成したコンテナの確認
$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                  PORTS                                     NAMES
672eeb5264c2        nginx:latest        "nginx -g 'daemon of   12 seconds ago      Up Less than a second   443/tcp, 192.241.211.126:32778->80/tcp    swarm-node2/web1
e6602df93c98        redis:latest        "/entrypoint.sh redi   24 seconds ago      Up Less than a second   6379/tcp, 192.241.233.78:6879->6879/tcp   swarm-node1/redis
400c04e5e5d1        nginx:latest        "nginx -g 'daemon of   2 minutes ago       Up 2 minutes            443/tcp, 192.241.228.14:32768->80/tcp     swarm-master/web2
```

2GBの容量しかない3ノードに2GBのコンテナを3つ立てたため、それぞれswarm-master、swarm-node1、swarm-node2に分散されて配置されています。

`docker info`で結果を見ると実際にメモリが予約されているのが分かります。

```bash:メモリの予約を確認
$ docker info
Containers: 7
Strategy: spread
Filters: affinity, health, constraint, port, dependency
Nodes: 3
 swarm-master: 192.241.228.14:2376
  └ Containers: 3
  └ Reserved CPUs: 0 / 2
  └ Reserved Memory: 2 GiB / 2.053 GiB
 swarm-node1: 192.241.233.78:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 2
  └ Reserved Memory: 2 GiB / 2.053 GiB
 swarm-node2: 192.241.211.126:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 2
  └ Reserved Memory: 2 GiB / 2.053 GiB
```

ここで同じ2GBでもう一台コンテナを作ろうとするとエラーになります。

```bash:容量を超えてコンテナは作れないのでエラーになる
docker run -p "80" -d --name web3 -e "affinity:container!=redis" -m 2g nginx
FATA[0000] Error response from daemon: no resources available to schedule container
```

この辺まで確認したら一旦コンテナを削除しておきます。

```bash:コンテナの削除
$ docker stop redis web1 web2
$ docker rm redis web1 web2
```

### Composeを使ってSwarmクラスタ上にコンテナを作成


続いてComposeと組み合わせてみます。
ベタな例ですがアプリのコンテナからredisのコンテナのカウンタをインクリメントして取得するものを作ってみます。

なお、ComposeとSwarmを連携する場合、残念ながら現状では[buildコマンドが使えない](https://github.com/docker/compose/releases/tag/1.2.0)ので、DockerHubまたはプライベートRegistryにあるイメージを使うしかなさそうです。

今回はアプリ側は以下のようなシンプルなgoのアプリを使います。
redisにアクセスしてkeyという名前のkeyを取得して表示するだけです。

```go:main.go
package main

import (
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/garyburd/redigo/redis"
	"github.com/zenazn/goji"
	"github.com/zenazn/goji/web"
)

var (
	rHost = os.Getenv("REDIS_HOST")
	pool  *redis.Pool
)

func newPool() *redis.Pool {
	return &redis.Pool{
		MaxIdle:     3,
		MaxActive:   10,
		IdleTimeout: 240 * time.Second,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", rHost+":6379")
			if err != nil {
				panic(err)
			}
			return c, err
		},
	}
}

func get(c web.C, w http.ResponseWriter, r *http.Request) {
	con := pool.Get()
	defer con.Close()
	n, _ := redis.Int(con.Do("INCR", "key"))
	fmt.Fprintf(w, "You have visited this page %#v times!\n", n)
}

func main() {
	pool = newPool()
	con := pool.Get()
	con.Do("PING")
	con.Close()

	goji.Get("/", get)
	goji.Serve()
}
```

Dockerfileは以下です。

```bash:Dockerfile
FROM golang:1.4.2
ADD app /app
CMD /app
```

これはビルドして[DockerHubにあげてあります](https://registry.hub.docker.com/u/daikikohara/app01/)。

docker-compose.ymlは以下のようにしています。

```yml:docker-compose.yml
redis:
  image: redis
  ports:
    - "6379:6379"
  environment:
    - "constraint:node==swarm-master" #必ずswarm-masterで起動するようにする
web:
  image: daikikohara/app01:v5 #何度か失敗してv5になっちゃいましたｗ
  restart: always #redisのコンテナよりも先に起動した場合はエラーで落ちるので再起動させる
  ports:
    - "8000"
  environment:
    - REDIS_HOST #環境変数で設定したREDIS_HOSTの値を使う。KEY=VALUEで指定しない場合、VALUEはOSの環境変数から読まれる。
    - "affinity:image!=redis" #redisと違うノードに立てる例
```

docker-compose単体では[links](https://docs.docker.com/compose/yml/#links)やexternal_linksを使ってredisと接続できますが、swarmでマルチノード上に構築する場合は現状できないようです。
そのため、ちょっと無理矢理な感じですが、アプリからは環境変数で設定したREDIS_HOSTにアクセスするようにしています。
ということで、無理矢理感満載ですが、REDIS_HOSTをswarm-masterのホストのIPにするため以下のように設定します。

```bash:REDIS_HOSTを設定
export REDIS_HOST=$(docker-machine env swarm-master | grep -o -e "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+")
```

なお、変数の展開も今はできませんが、[Issue](https://github.com/docker/compose/issues/1377)が開いているのでいずれ対応するかと思うので、環境変数渡しはしなくてよくなりそうです。
というか普通にlinksが使えるようになるとか、もっと良いやり方が出てくると思いますが。。。

ということで`docker-compose up -d`をして結果を確認してみます。
事前に`eval $(docker-machine env --swarm swarm-master)`をしてあればSwarmクラスタ上に配置してくれます。

```bash:出来上がったコンテナの確認
docker ps
CONTAINER ID        IMAGE                  COMMAND                CREATED             STATUS              PORTS                             NAMES
132530465f28        redis:latest           "/entrypoint.sh redi   33 seconds ago      Up 21 seconds       192.241.228.14:6379->6379/tcp     swarm-master/docker_redis_1
fe38440aaee9        daikikohara/app01:v5   "/bin/sh -c /app"      33 seconds ago      Up 9 seconds        192.241.211.126:32779->8000/tcp   swarm-node2/docker_web_1
```

ちゃんとComposeに書いたとおりに出来上がってますね。

アプリにcurlでアクセスしてもちゃんとカウンターが取れます。

```bash:アプリの動作確認
$ curl http://192.241.211.126:32779
You have visited this page 1 times!

$ curl http://192.241.211.126:32779
You have visited this page 2 times!
```

アプリ側のスケールアウトはこんな感じでできます。

```bash:アプリのスケールアウト
$ docker-compose scale web=3
```

```bash:結果確認
$ docker ps
CONTAINER ID        IMAGE                  COMMAND                CREATED             STATUS              PORTS                             NAMES
de32c9e4471a        daikikohara/app01:v5   "/bin/sh -c /app"      56 seconds ago      Up 51 seconds       192.241.211.126:32783->8000/tcp   swarm-node2/docker_web_3
c8f38ae93647        daikikohara/app01:v5   "/bin/sh -c /app"      56 seconds ago      Up 51 seconds       192.241.211.126:32782->8000/tcp   swarm-node2/docker_web_2
fe38440aaee9        daikikohara/app01:v5   "/bin/sh -c /app"      4 minutes ago       Up 4 minutes        192.241.211.126:32779->8000/tcp   swarm-node2/docker_web_1
132530465f28        redis:latest           "/entrypoint.sh redi   4 minutes ago       Up 3 minutes        192.241.228.14:6379->6379/tcp     swarm-master/docker_redis_1
```

webが3台になりました。
それぞれにアクセスしてもちゃんとカウンターが取れます。

```bash:スケールアウトさせたコンテナで動作確認
$ curl http://192.241.211.126:32782
You have visited this page 3 times!

$ curl http://192.241.211.126:32783
You have visited this page 4 times!
```

戻すときは`docker-compose scale web=1`とやってやればOKです。

とりあえずこんな感じです。最後に片付けをしておきましょう。

```bash:ノードの削除
docker-machine rm swarm-master swarm-node1 swarm-node2
```

## 次期バージョン

それぞれ以下のマイルストーンです。いずれも2015年6月16日リリース予定なのでもうすぐですね。
6月22,23には[Dockercon](http://www.dockercon.com/)も行われるのでまだまだDockerのネタは尽きることがなさそうです。

* [Docker 1.7](https://github.com/docker/distribution/wiki/docker-1.7-Project-Page)
* [Machine 0.3](https://github.com/docker/machine/wiki/0.3.0-Milestone-Project-Page)
* [Swarm 0.3](https://github.com/docker/swarm/wiki/0.3.0-Milestone-Project-Page)
* [Compose 1.3](https://github.com/docker/compose/wiki/1.3.0-Milestone-Project-Page)

個人的にはSwarmによるコンテナダウン時の自動リバランスが一番楽しみですね。

## 最後に

元figだったCompose以外はまだ出たばかりなのでまだまだこれからという印象ですが、かなり便利に使えるような印象でした。
いずれもDocker社の方針で"batteries included but removable/swappable"という原則に従ってます。
これはエコシステムの他社製ツール(MesosやKubernetes等)に配慮してDocker社としてはSwarm等のツールを出すけど、規模が大きくなった時等に他のツールに置き換え可能ですよっていう方針のようです。
実際に現状だとfailover等で明らかにDocker社からのエコシステムだけでは足りないので、他のツールを組み合わせる必要は有りますね。
ただ多分リップサービス的なところもある気がして、最終的にはDocker社としては全部Docker社製品で固めたいんじゃないかなぁって気がしなくもないですね。

以上。