Docker Swarmを試してみた


# 基礎知識
非常に今更ですが、そもそもコンテナ自体が初めてなので、基礎知識を確認。

## Docker
Linuxカーネルのもつコンテナ機能を利用して、アプリケーションのデプロイを自動化するためのソフトウェア。コンテナ仮想化の技術(コンテナエンジン)においてはデファクトスタンダード。
Comunity Edition(CE)とEnterprise Edition(EE)があり、EEの中でもBasic, Standard, Advancedがある。フリーでずっと使用できるのはCE。

Dockerそのものでは、自身のインストールされたホスト上のコンテナしか管理できないため、複数ホスト(クラスタ)上でコンテナを運用するには別途オーケストレーションツールが必要。オーケストレーションツールに関しては、どれがよいかはまだ議論がある。
[参考1](http://techgenix.com/swarm-kubernetes-mesos/)
[参考2](https://www.nelco.com/ja/orchestrater/)
[参考3](https://thinkit.co.jp/story/2015/04/10/5855)
→比較という意味ではこれが一番わかりやすい。

## Docker Swarm
Docker1.12より、Docker自体にバンドルされたオーケストレーションツール。オートスケーリングやリバランスの機能がないなど、他に比べると機能が劣るということだが…。Dockerだけで完結できるので、手軽さという意味ではよさそう。

## Kubernetes(k8s)
Googleが開発して、今はCloud Native Computing Foundationが管理。複数のコンテナをPodという単位にまとめ、Pod単位で管理を行う。またPodにServiceというエンドポイントを持たせることができるため、コンテナがスケールアウトしたり、別のホストに移動しても同一のURLでアクセスが継続できる。
KVS(Key Value Store)としてetcdを使用。

## Mesos
Apacheが運営。Mesosphere社はディストリビューションを提供。
他2つとは異なり、コンピューティングレイヤーでの抽象化を行う。別の言い方では、複数のホストを1つの巨大なホストのように見立てて、そのうえでコンテナを動かすという考え方。
またそもそもコンテナ管理ツールではないため、コンテナを動かすには別のツールとの連携が必要(Marathon,Chronos)。
KVS(Key Value Store)としてZooKeeperを使用。

# インストール
今回はDocker + Swarmで試してみる。

## インストール要件
https://docs.docker.com/engine/installation/linux/centos/

## 環境
CentOS 7(minimal)

## Install
[公式](https://docs.docker.com/engine/installation/linux/centos/)通りで問題なし。
追加で、
https://docs.docker.com/engine/installation/linux/linux-postinstall/#configure-docker-to-start-on-boot
の自動起動だけはやっておく。

## Tutrial
https://docs.docker.com/get-started/

### Image作成
以下3つのファイルを作成して、空きディレクトリに格納。
- Dockerfile
- requirements.txt
- app.py

```shell-session:Dockerfile
# Use an official Python runtime as a base image
FROM python:2.7-slim

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
ADD . /app

# Install any needed packages specified in requirements.txt
RUN pip install -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["python", "app.py"]
```

```shell-session:requirements.txt
Flask
Redis
```

```py:app.py
from flask import Flask
from redis import Redis, RedisError
import os
import socket

# Connect to Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)

app = Flask(__name__)

@app.route("/")
def hello():
    try:
        visits = redis.incr("counter")
    except RedisError:
        visits = "<i>cannot connect to Redis, counter disabled</i>"

    html = "<h3>Hello {name}!</h3>" \
           "<b>Hostname:</b> {hostname}<br/>" \
           "<b>Visits:</b> {visits}"
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)

if __name__ == "__main__":
        app.run(host='0.0.0.0', port=80)
```

そのディレクトリにて、イメージを作成。

```shell-session
[root@localhost testapp]# ls
app.py  Dockerfile  requirements.txt
[root@localhost testapp]#
[root@localhost testapp]# docker build -t friendlyhello .
Sending build context to Docker daemon 4.608 kB
Step 1/7 : FROM python:2.7-slim

[root@localhost testapp]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
friendlyhello       latest              d058024f6d16        38 seconds ago      195 MB
python              2.7-slim            1c7128a655f6        13 days ago         183 MB
hello-world         latest              48b5124b2768        4 months ago        1.84 kB
```

### 起動

```shell-session
[root@localhost testapp]# docker run -p 4000:80 friendlyhello
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
```

コンテナの視点ではポート80で起動しているが、ホストから見ると、ポート4000にマッピングして外部にエクスポートしている。そのため、外からブラウザでアクセスする際は4000に接続すればよい。
![20170525_023.jpg](https://qiita-image-store.s3.amazonaws.com/0/159980/2bb9d1d2-032d-22cf-5d34-fbb0622e6858.jpeg)

上記ではフォアグラウンドで起動してしまうが、バックグラウンドで起動するには、
`docker run -d -p 4000:80 friendlyhello`と-dをつける(Detach mode)。
停止は下記のように、docker psでプロセスIDを特定して、docker stopすればよい。

```shell-session
[root@localhost testapp]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
9a8a215706d0        friendlyhello       "python app.py"     6 seconds ago       Up 5 seconds        0.0.0.0:4000->80/tcp   festive_snyder
[root@localhost testapp]#
[root@localhost testapp]#
[root@localhost testapp]# docker stop 9a8a215706d0
9a8a215706d0
```


### イメージのシェア
Docker Cloudというサイトに登録すると、フリーでレポジトリを使用することができ、そこにイメージのアップロード、および公開が可能。
https://cloud.docker.com

初回アクセス時はSign Upして、適当なレポジトリを作成する。Docker IDがユーザ名に当たるが、大文字は使用不可らしい。

上記完了したら、Dockerホストにて以下を実行すると、タグ付けしたイメージがアップロードされる。
`# docker tag friendlyhello keisuke1208/test:test`
→friendlyhelloというイメージに、keisuke1208というアカウントの、testというレポジトリの、testというタグを割り当てている。

```shell-session
[root@localhost testapp]# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: keisuke1208
Password:
Login Succeeded
[root@localhost testapp]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
friendlyhello       latest              d058024f6d16        37 minutes ago      195 MB
python              2.7-slim            1c7128a655f6        13 days ago         183 MB
hello-world         latest              48b5124b2768        4 months ago        1.84 kB
[root@localhost testapp]#
[root@localhost testapp]# docker tag friendlyhello keisuke1208/test:test
[root@localhost testapp]# docker push keisuke1208/test:test
The push refers to a repository [docker.io/keisuke1208/test]
afe75cea1bdd: Pushed
a95f70df3cfb: Pushed
3f925f4b7c74: Pushed
7b7f69d6236f: Mounted from library/python
667e68ed0db3: Mounted from library/python
5eac2de68a97: Mounted from library/python
8d4d1ab5ff74: Mounted from library/python
test: digest: sha256:a1e7034a88bc3f272ea0ec78845a0c13824ce4635dc3b88e3b5a990c8adfd788 size: 1787
```

# Swarm
1.12以上であれば、何も意識しなくてもインストールされている。

## Serviceの設定
複数のコンテナにてある1つの「サービス」を提供するときに、これをserviceと定義する。Serviceの定義はdocker-compose.ymlというymlにて行う。以下は1例。
webという名前のserviceを、2台のコンテナで起動する。それぞれのコンテナはホストの0.1(10%)のCPUと、50MBのメモリを使用できる。ポートはコンテナ上の80をホストの4000にマップしている。
最初のversionはこのファイルの書式のバージョン。
書式の詳細は[こちら](https://docs.docker.com/compose/compose-file/)。

```shell-session:docker-compose.yml
version: "3"
services:
  web:
    image: keisuke1208/test:test
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "4000:80"
    networks:
      - webnet
networks:
  webnet:
```

ファイルを作成したら、swarmを起動したうえでservice(正確にはStack)を起動させる。
以下の例では、getstartedlabというstackを起動しているが、webというserviceはこのstackの一部。
stackはserviceをまとめたもので、1つのStackが複数のサービスを持つことができる。

```shell-session
[root@localhost testapp]# docker swarm init
Swarm initialized: current node (z2ng430u16l3ztxb7xpxneedl) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-262rqbrvz7gy0sylfdpxnizsd2u6q0qf5n5mg207khkhj0ll8r-4tsvhobo7tq0j4t74k7xhv6g1 \
    10.32.2.55:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

[root@localhost testapp]#
[root@localhost testapp]# docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web
[root@localhost testapp]#
[root@localhost testapp]# docker stack ps getstartedlab
ID            NAME                 IMAGE                  NODE                   DESIRED STATE  CURRENT STATE           ERROR  PORTS
rk2tqj9jqb5l  getstartedlab_web.1  keisuke1208/test:test  localhost.localdomain  Running        Running 13 seconds ago
r1n4klevxsn4  getstartedlab_web.2  keisuke1208/test:test  localhost.localdomain  Running        Running 12 seconds ago
[root@localhost testapp]#
```

docker stack psコマンドで、2つのコンテナが起動していることが確認できる。また4000番にアクセスすると、サービスも起動していることが確認できる。

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/125fa10c-b8cc-c98b-2525-1077a36fb2e3.png)

スケールアウトさせるには、ymlのreplicaの指定を変更して、再度stack deployすればよい。いったんダウンさせる必要はない。

```shell-session
[root@localhost testapp]# cat docker-compose.yml
version: "3"
services:
  web:
    image: keisuke1208/test:test
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "4000:80"
    networks:
      - webnet
networks:
  webnet:
[root@localhost testapp]#
[root@localhost testapp]#
[root@localhost testapp]#
[root@localhost testapp]# docker stack deploy -c docker-compose.yml getstartedlab
Updating service getstartedlab_web (id: mq21pewumaemvlw0ckkpkqsx2)
[root@localhost testapp]#
[root@localhost testapp]# docker stack ps getstartedlab
ID            NAME                 IMAGE                  NODE                   DESIRED STATE  CURRENT STATE           ERROR  PORTS
rk2tqj9jqb5l  getstartedlab_web.1  keisuke1208/test:test  localhost.localdomain  Running        Running 26 minutes ago
r1n4klevxsn4  getstartedlab_web.2  keisuke1208/test:test  localhost.localdomain  Running        Running 26 minutes ago
u336wzrj40we  getstartedlab_web.3  keisuke1208/test:test  localhost.localdomain  Running        Running 6 seconds ago
```

本当に大丈夫なのか、別のホストからncでウォッチしてみたところ、一瞬だけ落ちているようだが、基本的には問題なさそう。

```shell-session
[root@tsslin01 ~]# while true; do echo -en "GET / HTTP/1.1\n\n" | nc 10.32.2.55 4000 | head -n 1 >> temp3; date >> temp3;sleep 1;done
^C
[root@tsslin01 ~]#
[root@tsslin01 ~]# cat temp3
HTTP/1.0 200 OK
Thu May 25 16:19:30 JST 2017
HTTP/1.0 200 OK
Thu May 25 16:19:31 JST 2017
HTTP/1.0 200 OK
Thu May 25 16:19:32 JST 2017
Thu May 25 16:19:33 JST 2017
HTTP/1.0 200 OK
Thu May 25 16:19:34 JST 2017
```

# SwarmでCluster化
[参考](https://docs.docker.com/engine/swarm/join-nodes/)
もう一台同じようにDockerをインストールしたホスト(rainy)を用意。InstallまででTutrialの内容は未実施。
まずはManagerノード(元々swarm initしたホスト,sunny)にて状態確認。

```shell-session
[root@sunny ~]# docker node ls
ID                           HOSTNAME             STATUS  AVAILABILITY  MANAGER STATUS
z2ng430u16l3ztxb7xpxneedl *  sunny.weather.local  Ready   Active        Leader
```

Join用のトークンを発行。今回はWorkerとして追加。

```shell-session
[root@sunny ~]# docker swarm join-token worker
To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-262rqbrvz7gy0sylfdpxnizsd2u6q0qf5n5mg207khkhj0ll8r-4tsvhobo7tq0j4t74k7xhv6g1 \
    10.32.2.55:2377
```

表示されたコマンドをrainyにて実行。

```shell-session
[root@rainy ~]# docker swarm join --token SWMTKN-1-262rqbrvz7gy0sylfdpxnizsd2u6q0qf5n5mg207khkhj0ll8r-4tsvhobo7tq0j4t74k7xhv6g1 10.32.2.55:2377
This node joined a swarm as a worker.
```

これでSwarm Clusterが形成される。

```shell-session
[root@sunny ~]# docker node ls
ID                           HOSTNAME             STATUS  AVAILABILITY  MANAGER STATUS
kyjd450c6ixluglc6ixu8arwo    rainy.weather.local  Ready   Active
z2ng430u16l3ztxb7xpxneedl *  sunny.weather.local  Ready   Active        Leader
```

この状態でStackをデプロイすると、3台のコンテナ中2台がrainy,1台がsunnyで起動していることがわかる。

```shell-session
[root@sunny testapp]# docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web
[root@sunny testapp]#
[root@sunny testapp]# docker stack ps getstartedlab
ID            NAME                 IMAGE                  NODE                 DESIRED STATE  CURRENT STATE             ERROR  PORTS
jmzusd2ta971  getstartedlab_web.1  keisuke1208/test:test  rainy.weather.local  Running        Preparing 36 seconds ago
4j91vu2pn91q  getstartedlab_web.2  keisuke1208/test:test  rainy.weather.local  Running        Preparing 36 seconds ago
qreob82mno3p  getstartedlab_web.3  keisuke1208/test:test  sunny.weather.local  Running        Running 35 seconds ago
```

この状態でアクセスすると、ラウンドロビンで3台にアクセスされていることがわかる。
![image](https://qiita-image-store.s3.amazonaws.com/0/159980/22d3460d-815f-e34d-ea0b-97cdc8bb9d1e.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/159980/3bfd5af8-54e8-2e1f-eae1-b3347a2cceab.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/159980/c39843c3-7764-ed1c-2251-7ed2b0f84538.png)

ここで表示されるホスト名は、各コンテナに設定されているらしい。

```shell-session
[root@sunny testapp]# ^ls^cat
cat /var/lib/docker/containers/f71ab01c50019138939cc897906c690f518c8325086f9b6c274a486f5a78ee15/hostname
f71ab01c5001
```

## 障害が発生すると??
Worker(rainy)のdockerサービスを落として疑似障害を発生させる。

```shell-session
[root@sunny testapp]# docker stack ps getstartedlab
ID            NAME                 IMAGE                  NODE                 DESIRED STATE  CURRENT STATE          ERROR  PORTS
i572m3d8ngpa  getstartedlab_web.1  keisuke1208/test:test  rainy.weather.local  Running        Running 7 seconds ago
w2yd90vqk3e5  getstartedlab_web.2  keisuke1208/test:test  sunny.weather.local  Running        Running 7 seconds ago
nqdoy8a7ymy4  getstartedlab_web.3  keisuke1208/test:test  sunny.weather.local  Running        Running 7 seconds ago
[root@sunny testapp]#
(Worker(rainy)のDockerサービスをStop)
[root@sunny testapp]#
[root@sunny testapp]# docker stack ps getstartedlab
ID            NAME                     IMAGE                  NODE                 DESIRED STATE  CURRENT STATE                   ERROR  PORTS
x66fmwazzoxy  getstartedlab_web.1      keisuke1208/test:test  sunny.weather.local  Running        Running less than a second ago
i572m3d8ngpa   \_ getstartedlab_web.1  keisuke1208/test:test  rainy.weather.local  Shutdown       Running 20 seconds ago
w2yd90vqk3e5  getstartedlab_web.2      keisuke1208/test:test  sunny.weather.local  Running        Running 39 seconds ago
nqdoy8a7ymy4  getstartedlab_web.3      keisuke1208/test:test  sunny.weather.local  Running        Running 39 seconds ago
[root@sunny testapp]#
(Worker(rainy)のDockerサービスをStart)

[root@sunny testapp]# docker node ls
ID                           HOSTNAME             STATUS  AVAILABILITY  MANAGER STATUS
kyjd450c6ixluglc6ixu8arwo    rainy.weather.local  Ready   Active
z2ng430u16l3ztxb7xpxneedl *  sunny.weather.local  Ready   Active        Leader
[root@sunny testapp]#
[root@sunny testapp]# docker stack ps getstartedlab
ID            NAME                     IMAGE                  NODE                 DESIRED STATE  CURRENT STATE               ERROR  PORTS
x66fmwazzoxy  getstartedlab_web.1      keisuke1208/test:test  sunny.weather.local  Running        Running about a minute ago
i572m3d8ngpa   \_ getstartedlab_web.1  keisuke1208/test:test  rainy.weather.local  Shutdown       Shutdown 12 seconds ago
w2yd90vqk3e5  getstartedlab_web.2      keisuke1208/test:test  sunny.weather.local  Running        Running 2 minutes ago
nqdoy8a7ymy4  getstartedlab_web.3      keisuke1208/test:test  sunny.weather.local  Running        Running 2 minutes ago
```

このshutdownとなっている古いプロセスの情報は、任意には消せないらしい。
`docker service update <service name> --force`
でリバランスは可能だが、生きているやつもすべて落としてリバランスしている。

```shell-session
[root@sunny testapp]# docker service update getstartedlab_web --force
getstartedlab_web
[root@sunny testapp]#
[root@sunny testapp]# docker service ps getstartedlab_web
ID            NAME                     IMAGE                  NODE                 DESIRED STATE  CURRENT STATE           ERROR  PORTS
qnkadpxcvu9f  getstartedlab_web.1      keisuke1208/test:test  rainy.weather.local  Ready          Ready 2 seconds ago
x66fmwazzoxy   \_ getstartedlab_web.1  keisuke1208/test:test  sunny.weather.local  Shutdown       Running 2 seconds ago
i572m3d8ngpa   \_ getstartedlab_web.1  keisuke1208/test:test  rainy.weather.local  Shutdown       Shutdown 4 minutes ago
y24fw1ggg3lc  getstartedlab_web.2      keisuke1208/test:test  rainy.weather.local  Ready          Ready 2 seconds ago
w2yd90vqk3e5   \_ getstartedlab_web.2  keisuke1208/test:test  sunny.weather.local  Shutdown       Running 2 seconds ago
vsftpfdx56y3  getstartedlab_web.3      keisuke1208/test:test  sunny.weather.local  Ready          Ready 2 seconds ago
nqdoy8a7ymy4   \_ getstartedlab_web.3  keisuke1208/test:test  sunny.weather.local  Shutdown       Running 2 seconds ago
```

# 感想
他を使ったことがないのであまり参考にならないが、Docker + Swarmは確かに簡単そう。複数台ホストに分散したシステムも簡単に作れた。ドキュメントの精度も高く、ほとんどつっかかるポイントがなかったのも助かる。
リバランスが乱暴なのが気になるが、もう少し使ってみないと何ともいえない。

# その他参考
KVS比較
https://www.consul.io/intro/vs/zookeeper.html

Docker CE,EE
https://www.slideshare.net/Docker/docker-online-meetup-announcing-docker-ce-ee

Docker Doc
https://docs.docker.com