Docker Swarmで学ぶサービスメッシュ

# はじめに
本記事はDocker Swarmでクラスタを構築し、サービスメッシュについて学びます。

環境はAWSのELB（Elastic Load Balancing）と、EC2（Amazon EC2）インスタンス2台を使用し、Docker Swarmのクラスタを構築します。

![Qiita_swarm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/a068c1b0-2252-2bc5-46d8-bf793cad7cf1.png)

Kubernetesと比較した際にDocker Swarmを導入するメリットは、導入コストが低いことです。Docker SwarmはKubernetesに比べてハードルが低いため、本番環境にコンテナ技術を導入しようとしているシステムには最適だと考えます。

**Docker Swarmを通してサービスメッシュの意義を体感しましょう。**

## Docker Swarm
Docker Swarmは、Docker社が提供するオーケストレーションツールです。
Docker Swarmを使用することで複数のホストを集約し、簡単にコンテナのデプロイとスケールが実現できます。

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/3362ea8b-2a05-8729-a1da-c7e83023f405.png)

Docker Swarmはマネージャとノードで構成され、**Swarm**は群れを意味します。
Docker Swarのクラスタを構築するためには、以下の作業が必要です。

- [x] マネージャとノードにDockerをインストールする。
- [x] マネージャとノード間で通信ができるように、ファイアウォールで必要な通信を許可する。

- ファイアウォールで許可するルール

| ポート番号       |用途        |
|:-----------------|:------------------|
| 2377/tcp         |クラスタ管理用の通信 |
| 4789/udp         |オーバーレイ・ネットワーク |
| 7946/tcp         |ノード間通信 |
| 7946/udp         |ノード間通信 |

## サービスメッシュ
サービスメッシュはマイクロサービスアーキテクチャを前提とし、アプリケーション間で通信を制御する仕組みを提供します。この制御された通信はメッシュの様に編みがけになっているため、信頼性を向上します。

例として2台構成のホストの場合、通常のアクセスパスは以下になります。

ブラウザでロードバランサーのDNS名にアクセスすると、HTTPリクエストを受け付けたロードバランサーは、ラウンドロビンでバックエンドであるホストの公開ポートにHTTPリクエストを振り分けます。

![swarm2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/483c4c9c-3cf5-43a8-b814-b19a103cb8f1.png)

例えば、片方のホストで何らかの障害が発生し、コンテナがダウンした場合でも以下の経路により、サービスを継続することができます。

![swarm2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/d2975e04-1cb5-6257-1324-18dd1ebabd6a.png)

要約すると、単純にホストレベルでアプリケーションを分ける構成の場合は、片方のホストに何らかの障害が発生したときにサービスの提供ができなくなります。よって、コンテナ技術を活用し、サービスメッシュにすることで単一障害点をなくすことができます。

## Docker Swarm構築
Docker Swarmの構築手順について記載します。
Docker Swarmの構築は、マネージャ、ノードの順に作業を行います。

本記事の例ではweb1がマネージャ、web2がノードになり、Nginxのイメージを使用してデプロイします。

前提条件としてDockerは既にインストールされた状態で、上記で解説したファイアウォールも許可されていることとします。また、本記事では最低限の構成としているため、マネージャ1台、ノード1台になります。

### マネージャ
まずはじめに、Docker Swarmの初期化を行います。`docker swarm init`コマンドを実行することで**Swarmモード**が有効になります。

複数IPアドレスを持つ場合は、他のノードとの通信で使用するインターフェースのIPアドレスを`advertise-addr`の引数に指定します。

- Docker Swarmの初期化   
`# docker swarm init --advertise-addr  ＜IPアドレス＞`

```console
Swarm initialized: current node (zirc78nsch77ox8di6021ux4n) is now a manager.

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

    docker swarm join --token SWMTKN-1-459p0pkyqgxgkhaggjhavd419ldujenqttm1mqmwup0qz9m5qv-1kj3jy6ozwrr2fkj1qvas294a ＜マネージャのIPアドレス＞:2377

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

コマンド実行後、出力結果の`docker swarm join --token SWMTKN-1-（略） ＜マネージャのIPアドレス＞:2377`を控えます。

- node確認   
`# docker node ls`   

```console
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
zirc78nsch77ox8di6021ux4n *   web1          Ready               Active              Leader              18.09.9-ce
```

マネージャの場合は、MANAGER STATUSに**Leader**と出力されます。

- ネットワーク確認  
`# docker network ls`

```console
NETWORK ID          NAME                DRIVER              SCOPE
53703efd3d07        bridge              bridge              local
aacf6f5e0eb4        docker_gwbridge     bridge              local
1f0d0e4ae3e7        host                host                local
xip5tlqmokpb        ingress             overlay             swarm
2d36f1c8c80f        none                null                local
```

Swarmの初期化を行うと新たに２つのネットワーク（docker_gwbridge、ingress）が作成されます。

### ノード
次にノードをDocker Swarmのクラスタに参加させるため、ノード側で以下のコマンドを実行します。

`--token`の引数にしている値は例になります。上記`docker swarm init`コマンドの出力結果をコピーしてペーストすれば大丈夫です。

なお、マネージャ側で`docker swarm join-token worker`コマンドを実行することで、トークンの再表示もできます。

- クラスタ参加   
`# docker swarm join --token SWMTKN-1-（略） ＜マネージャのIPアドレス＞:2377`

```console
This node joined a swarm as a worker.
```

マネージャ側で確認のため、再度、`docker node ls`コマンドを実行すると、nodeが認識されていることが確認できます。

- node確認

```console
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
zirc78nsch77ox8di6021ux4n *   web1          Ready               Active              Leader              18.09.9-ce
n2o22ptdmyhan8qg0ijmo0qn5     web2          Ready               Active                                  18.09.9-ce
```

## Docker Swarmのデプロイ
本記事では管理しやすいdocker-composeでデプロイします。
また、`docker service create`コマンドでもデプロイできます。serviceやstackの説明については割愛します。

デプロイ作業はマネージャ側で行います。
任意のディレクトリに移動し、以下の`docker-compose.yml`ファイルを作成します。

- docker-compose.yml

```docker
version: "3"
services:
  web:
    image: nginx
    deploy:
      replicas: 2
      #resources:
      #  limits:
      #    cpus: "0.1"
      #    memory: 100M
      restart_policy:
        condition: on-failure
    ports:
      - "80:80"
    networks:
      - webnet
networks:
  webnet:
```

`docker-compose.yml`ファイル作成後、以下のコマンドを実行し、デプロイします。
`test`は例となるため、任意の名前を指定します。

`# docker stack deploy -c docker-copose.yml test`

```console
Updating service test_web (id: egubeieuri00rzmm9imsna93o)
```

デプロイ後、以下のコマンドでサービスの状態が確認できます。
**REPLICAS**は2となっているので、2つのコンテナが起動しています。

`# docker service ls`

```console
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
r04mfg1se3nh        test_web            replicated          2/2                 nginx:latest        *:80->80/tcp
```

マネージャとノード側で`docker container ps -a`コマンドを実行すると、コンテナが起動しているのが確認できます。

- マネージャ側

```console
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
4a26c3ca6df7        nginx:latest        "nginx -g 'daemon of…"   3 seconds ago       Up 2 seconds        80/tcp              test_web.1.mnnz40tdykzd2intwz5hf68bs
```

- ノード側

```console
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
614c19349bf0        nginx:latest        "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        80/tcp              test_web.2.6om5oazbavassohd4akucbum2
```

## Docker Swarmの動作確認
Docker Swarmの動作確認を行います。

ブラウザからロードバランサーのDNS名にアクセスを行い、正常に負荷分散されることを確認します。

- ロードバランサーのDNS名にアクセス

<img width="1440" alt="スクリーンショット 2020-02-05 23.37.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/61e52786-b3f9-596a-b58d-3c296cfe33c3.png">

コンテナのログを確認すると、ラウンドロビンで負荷分散されているのが確認できます。
**CONTAINER ID**は`docker container ps -a`のコマンドで確認します。

`# docker logs -f ＜CONTAINER ID＞`

```console
10.255.0.3 - - [06/Feb/2020:14:20:51 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" "＜アクセス元のグローバルIP＞"
```

試しにweb2上（ノード側）で稼働しているコンテナを停止します。

`# docker stop ＜CONTAINER ID＞`

```console
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
423f673b0513        nginx:latest        "nginx -g 'daemon of…"   19 hours ago        Exited (0) 5 seconds ago                       test_web.2.kc7yypyasgvjtolsb1zwmhjoy
```

このときweb2上でコンテナは稼働していません。
ロードバランサーのDNS名にアクセスできることを確認します。

<img width="1440" alt="alb-acces.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/fe9bc485-706f-a3d5-a976-6ed5c8087d36.png">

例としてブラウザから停止したweb2の公開IPにアクセスします。

<img width="1440" alt="web2-acces.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/9754ef7f-3e7b-1599-fc75-a97bf2a4f2f8.png">

Web1（マネージャ）上のコンテナのアクセスログに、停止したweb2（ノード）の公開IPに対するアクセスが確認できます。

```console
10.255.0.3 - - [07/Feb/2020:02:19:01 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" "-"
```

停止したweb2（ノード側）のローカルからも、以下のコマンドを実行することでコンテナにアクセスができます。

`# curl localhost 80`

```console
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
curl: (7) Couldn't connect to server
```

web1（マネージャ）のログでは以下の様に出力されます。

```console
10.255.0.2 - - [07/Feb/2020:02:41:47 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.61.1" "-"
```

web1（マネージャ）で4789ポートをダンプして見ていると、オーバーレイ・ネットワークの通信を見ることができます。

`# tcpdump -nli eth0 port 4789 -Av`

## Docker Swarmのスケール
既にサービスが起動している状態で、以下のコマンドを実行することでスケールができます。以下のコマンドはコンテナの数を4コンテナに変更しています。

`# docker service scale test_web=4`

```console
test_web scaled to 4
overall progress: 4 out of 4 tasks 
1/4: running   [==================================================>] 
2/4: running   [==================================================>] 
3/4: running   [==================================================>] 
4/4: running   [==================================================>] 
verify: Service converged 
```

スケール後、`docker container ps -a`コマンドを実行すると、コンテナが増えていのが分かります。

```console
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
41962946aa48        nginx:latest        "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        80/tcp              test_web.4.pytl36ejcd81ybthisxfo47br
423f673b0513        nginx:latest        "nginx -g 'daemon of…"   7 hours ago         Up 7 hours          80/tcp              test_web.2.kc7yypyasgvjtolsb1zwmhjoy
```

**デプロイ時はサーバ1台ずつに対してアプリケーション資産の入れ替えを行う必要がなく、マネージャ1台で済みます。**

## Docker Swarmの性質
Docker Swarmは冪等性を持っています。

具体的には、Docker Swarmは指定したreplicas数のコンテナを維持するため、クラスタ化している片方のホストがダウンした場合は、片方のホストでコンテナを起動します。

以下は`docker container ps -a`コマンドの出力になります。
例としてweb1とweb2でそれぞれコンテナが起動しています。

- web1

```console
e5ccbfa9739b        nginx:latest                      "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        80/tcp              test_web.1.mrgkhbd7juer72v6bv0l42fxq
```

- web2

```console
4820c7bbe9c1        nginx:latest                      "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        80/tcp              test_web.2.wfe1n11s8940rdl8r1c47o6nc
```

web2のホストを停止しました。web2のダウンを検知すると、web1上でコンテナを作成します。

```console
0a88f53039a3        nginx:latest                      "nginx -g 'daemon of…"   5 seconds ago       Created                                 test_web.2.p06zas3c3kt9ekjojhhfnl3co
e5ccbfa9739b        nginx:latest                      "nginx -g 'daemon of…"   4 minutes ago       Up 4 minutes        80/tcp              test_web.1.mrgkhbd7juer72v6bv0l42fxq
```

web1上でコンテナが2台起動しています。

```console
0a88f53039a3        nginx:latest                      "nginx -g 'daemon of…"   37 seconds ago      Up 31 seconds       80/tcp              test_web.2.p06zas3c3kt9ekjojhhfnl3co
e5ccbfa9739b        nginx:latest                      "nginx -g 'daemon of…"   5 minutes ago       Up 5 minutes        80/tcp              test_web.1.mrgkhbd7juer72v6bv0l42fxq
```

## Docker Swarm解除
Docker Swarmの解除方法について記載します。

先にマネージャ側で以下のコマンドを実行し、サービスの削除を行いす。

`# docker service rm test_web`

```console
test_web
```

次にノード、マネージャの順に作業を行います。

### ノード

- ノードの切り離し   
`# docker swarm leave`

```console
Node left the swarm.
```

### マネージャ
ノードのSTATUSが**DOWN**になったことを確認します。

- node確認   
`# docker node ls`

```console
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
zirc78nsch77ox8di6021ux4n *   web1          Ready               Active              Leader              18.09.9-ce
n2o22ptdmyhan8qg0ijmo0qn5     web2          Down                Active                                  18.09.9-ce
```

ノードを削除します。オプションの引数には、ノード名を指定します。

- node削除   
`# docker node rm --force web2`

```console
web2
```

マネージャからノードが認識されなくなったことを確認します。

- node確認   
`# docker node ls`

```console
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
zirc78nsch77ox8di6021ux4n *   web1          Ready               Active              Leader              18.09.9-ce
```

最後にマネージャ自身を切り離します。

- マネージャの切り離し   
`# docker swarm leave --force`

```console
Node left the swarm.
```

## Docker Swarm解体
Docker Swarmを使用しない場合は、マネージャ側で以下の作業を行います。

ノードが存在しないことを確認します。

- node確認   
`# docker node ls`

```console
Error response from daemon: This node is not a swarm manager. Use "docker swarm init" or "docker swarm join" to connect this node to swarm and try again.
```

Docker Swarmで作成されたネットワークが削除されたことを確認します。docker_gwbridgeのネットワークは残っています。

`# docker network ls`

```console
NETWORK ID          NAME                DRIVER              SCOPE
987cfc73d87c        bridge              bridge              local
aacf6f5e0eb4        docker_gwbridge     bridge              local
1f0d0e4ae3e7        host                host                local
2d36f1c8c80f        none                null                local
```

以下のコマンドを実行し、使用していない全てのリソース削除します。

- リソース削除   
`# docker system prune`

```console
WARNING! This will remove:
        - all stopped containers
        - all networks not used by at least one container
        - all dangling images
        - all dangling build cache
Are you sure you want to continue? [y/N] y
Deleted Networks:
docker_gwbridge

Deleted Images:
untagged: nginx@sha256:ad5552c786f128e389a0263104ae39f3d3c7895579d45ae716f528185b36bc6f
deleted: sha256:2073e0bcb60ee98548d313ead5eacbfe16d9054f8800a32bedd859922a99a6e1
deleted: sha256:a3136fbf38691346715cac8360bcdfca0fff812cede416469653670f04e2cab0
deleted: sha256:99360ffcb2da18fd9ede194efaf5d4b90e7aee99f45737e918113e6833dcf278
deleted: sha256:488dfecc21b1bc607e09368d2791cb784cf8c4ec5c05d2952b045b3e0f8cc01e
untagged: nginx@sha256:70821e443be75ea38bdf52a974fd2271babd5875b2b1964f05025981c75a6717
deleted: sha256:5ad3bd0e67a9c542210a21a3c72f56ef6387cf9b7f4c2506d2398d55a2593ed0
deleted: sha256:b69e2ed46519bc33e7c887967e4f61a2ee53aef165b70f75e208937fb42e7b4c
deleted: sha256:4cb7f732537bf0f65cd9f8f7b63bbe71abcf9d0df396f58621ef3be0b2487b27
deleted: sha256:556c5fb0d91b726083a8ce42e2faaed99f11bc68d3f70e2c7bbce87e7e0b3e10

Total reclaimed space: 253.4MB
```

## ナレッジ
### docker-composeのバージョン
Docker Swarmを使用する場合に、docker-composeで使用できるバージョンは**3**になります。

### docker-composeでビルドする場合の留意事項
docker-composeでビルド（stack）する場合、イメージが必要になります。

[Compose file version 3 referenceより](https://docs.docker.com/compose/compose-file/#build)
>注：（ バージョン3）Composeファイルを使用してSwarmモードでスタックをデプロイする場合、このオプションは無視され ます。このdocker stackコマンドは、ビルド済みのイメージのみを受け入れます。

そのため、**Docker registry**の環境が必要になります。

ローカルでDockerレジストリをセットアップする方法については、公式の以下URLが参考になります。

- [Deploy a stack to a swarm](https://docs.docker.com/engine/swarm/stack-deploy/)


例として、以下の様にビルドを指定してデプロイを実行した場合、

```text
version: '3'

services:
  app:
    build: ./src
```

以下のエラーメッセージが出力されて、デプロイをすることはできません。

```console
failed to create service stackdemo_backend: Error response from daemon: rpc error: code = InvalidArgument desc = ContainerSpec: image reference must be provided
```

### デプロイ時のエラー
例として2台構成のホストで、docker-compose.ymlファイルの**replicas**の値を2にしてデプロイした場合は、基本的に1ホストに対して1コンテナで分散されます。

もし、デプロイ時に片方のホストで2台のコンテナが起動した場合は、何らかのエラーが発生し、片方のホストでコンテナが起動できなかったことが考えられます。エラーはLinuxの場合、シスログから確認することができます。

### 本記事の構成（Master1台とNode1台）で検証した結果について
本記事の構成（Master1台とNode1台）で検証した結果について、備忘として記載します。

端的に言うと、Master1台とNode1台では真の冗長化は実現できません。

更に信頼性を上げる場合は、Master2台で冗長化を行う必要があります。Masterの冗長化については、公式に記載があります。

- **本記事の構成（Master1台とNode1台）で片方のコンテナを停止または削除したときの挙動**
Master及びNodeの片方のコンテナを停止または削除した場合は、`docker-compose.yml`ファイルの**replicas**で定義した数になるまで自己再生します。また、この瞬断が発生している間に公開IPにアクセスしてもサービスメッシュ経由でアクセスできます。しかし、localだと、一瞬瞬断が発生します。

- **本記事の構成（Master1台とNode1台）でNodeのホスト自体を停止したときの挙動**
Master側に`docker-compose.yml`ファイルの**replicas**で定義した数になるまで、Nodeで稼働していたコンテナが作成されます。（動きとしては、VmwareのVmotionに似ていますが、実態は違います。）
その後、Nodeを起動してもNodeで稼働していたコンテナは自動復旧しないため、手動の対応が必要です。ちなみにMasterで先ほど作成されたコンテナを停止または削除すると、Node側でまたコンテナが作成されます。

- **本記事の構成（Master1台とNode1台）でMasterのホスト自体を停止したときの挙動**
Docker SwarmのMasterは停止していますが、サービスメッシュによりアクセスはできるのでアプリケーションとして継続することができます。

# おわりに
不確実性があり変化の速さが求めれられる現代のアプリケーション開発には、オーケストレーションツールは必須な技術です。

応用としてサーバレスの技術を使用し、CPUやメモリリソース等のしきい値等を設定して、しきい値を超えたらサーバをスケールアウトなどの運用も行うことができます。

## 参考
- [Docker Swarm](http://docs.docker.jp/swarm/toc.html)
- [サービスメッシュとは](https://www.redhat.com/ja/topics/microservices/what-is-a-service-mesh)