Docker を使う（docker swarm でアプリケーションをデプロイする）

※ 2021/11/29 更新
環境を新しくしたため、説明を加筆、修正しました。

[前回の記事](https://qiita.com/landwarrior/items/5cfbf2ce07be812e0907) の続きです。
今回は、コンテナオーケストレータの一つである Docker Swarm を使い、アプリケーション構築を実施します。
Dockerの説明に注力するため、GitHubにて今回説明に使用するコードを公開しています。

https://github.com/landwarrior/flask-restful

こちらをダウンロードしてご確認いただければと思います。

# 前提条件

今回は、以下の環境で構築します。

- Windows 11 Pro バージョン 21H2
- VirtualBox 6.1.28
- Vagrant 2.2.18

Vagrant で構築した AlmaLinux8.4 に docker をインストールして docker swarm の環境を構築します。

# 構築の流れ

大まかに、以下の流れで構築していきます。

1. Vagrant で、 VM を 3 台分構築
1. それぞれの VM にログインし、それぞれの構築用に準備したシェルスクリプトを実行
1. 2 台目の docker 環境を 1 台目の docker swarm へ join させる

実際に手を動かすのはこれだけです。
シェルスクリプトで行われるのは AlmaLinux の準備と、 Chef Infra Client での自動構築です。
最終的に以下のような形に構築されます。

![構築イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/b7d7c0bc-a9dd-3bc9-923a-65c33cbaeaea.png)

アプリケーションは、 Python のフレームワークである Flask-RESTful を使用して REST API を構築しています。
Web サーバとして Nginx を使用しています。
AlmaLinux の 80 番ポートで受けた HTTP 通信を Docker 上の Nginx に流し、そこから Flask へプロキシし、結果を Nginx 経由で返します。
Docker のネットワーク的には以下のイメージになります。

![Docker Network](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/5acb4aa9-da07-082f-dbe6-2882117ae615.png)

この図で言いたいのは、 Docker Swarm で構築される Network はホスト（ここでは AlmaLinux のこと）を意識しなくていい、ということです。
Nginx はそれぞれのホストとポートを共有する関係上、ホストと 1:1 の関係になりますが、その先の Flask のコンテナはホストを意識せずにロードバランスされます。

構築されるものがどういった構成か、イメージがついたでしょうか。
ここからは実際に手を動かしていきます。

# VM の準備

前提条件に記載した環境が整っている前提で進めます。
バージョンは必ずしも合っていなくても問題ないかと思いますが、 VirtualBox のバージョンが新しくなると Vagrant がバージョンアップするまで対応していないこともあるので、 Vagrant の最新バージョンがどの VirtualBox のバージョンまで対応しているかを確認してからインストールすることをお勧めします。

## フォルダの準備

VM は 3 台必要なので、フォルダを 3 つ用意しましょう。
自分の場合は、以下のようにしました。

- D:\VirtualMachines\Vagrant\DockerTest\primary
- D:\VirtualMachines\Vagrant\DockerTest\secondary
- D:\VirtualMachines\Vagrant\DockerTest\mysql

フォルダ名は自身が分かればなんでもいいと思いますが、上記のフォルダ構成と仮定して進めます。
上記と異なる場合、適宜読み替えてください。

## Vagrantfile の準備

先に box を追加しておきます。
コマンドプロンプトを立ち上げ、以下のコマンドを実行します。

```console:コマンドプロンプト
vagrant box add bento/almalinux-8.4
```

次に、コマンドプロンプトで以下のコマンドを実行していきます。

```console:コマンドプロンプト
cd /d D:\VirtualMachines\Vagrant\DockerTest\primary
vagrant init bento/almalinux-8.4
cd ..\secondary
vagrant init bento/almalinux-8.4
cd ..\mysql
vagrant init bento/almalinux-8.4
```

それぞれのフォルダ内に作成された `Vagrantfile` を修正します。
アプリケーションの都合上、 IP アドレスは指定があります。
すでに使用済みの IP アドレスの場合、任意の IP アドレスを指定し、適宜読み替えてください。
アプリケーション上でも IP アドレスを記載している箇所があるので、その点の修正も忘れずに。
メモ帳やサクラエディタなどのテキストエディタで Vagrantfile を開き、修正します。

### primary の場合

- box バージョンの指定
    - `config.vm.box = "bento/almalinux-8.4"` という記載の下に `config.vm.box_version = "202109.10.0"` を追記します。これは、ほかのバージョンで動くか分からないために固定しています。
- IP アドレスの設定
    - `config.vm.network` のコメントアウトを外し、 `config.vm.network "private_network", ip: "192.168.33.10"`  
とします
- ホスト名を付ける
    - `config.vm.network` の下に、 `config.vm.hostname = "flask-primary"` を追加します
- 同期フォルダの設定
    - GitHub から取得いただいたソースを VM 上にマウントするため、 `config.vm.synced_folder` の部分を `config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"` のように指定します（ソース配置場所は適宜読み替えてください）
        - バックスラッシュではなくスラッシュに変更する必要あり

### secondary の場合

基本的に primary と同じです。

- box バージョンの指定
    - `config.vm.box = "bento/almalinux-8.4"` という記載の下に `config.vm.box_version = "202109.10.0"` を追記します。これは、ほかのバージョンで動くか分からないために固定しています。
- IP アドレスの設定
    - `config.vm.network` のコメントアウトを外し、 `config.vm.network "private_network", ip: "192.168.33.11"`  
とします（primary との相違点）
- ホスト名を付ける
    - `config.vm.network` の下に、 `config.vm.hostname = "flask-secondary"` を追加します（primary との相違点）
- 同期フォルダの設定
    - GitHub から取得いただいたソースを VM 上にマウントするため、 `config.vm.synced_folder` の部分を `config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"` のように指定します（ソース配置場所は適宜読み替えてください）
        - バックスラッシュではなくスラッシュに変更する必要あり

### mysql-server の場合

こちらもほぼ primary と同じです。

- box バージョンの指定
    - `config.vm.box = "bento/almalinux-8.4"` という記載の下に `config.vm.box_version = "202109.10.0"` を追記します。これは、ほかのバージョンで動くか分からないために固定しています。
- IP アドレスの設定
    - `config.vm.network` のコメントアウトを外し、 `config.vm.network "private_network", ip: "192.168.33.20"`  
とします（primary との相違点）
- ホスト名を付ける
    - `config.vm.network` の下に、 `config.vm.hostname = "mysql-server"` を記載します（primary との相違点）
- 同期フォルダの設定
    - GitHub から取得いただいたソースを VM 上にマウントするため、 `config.vm.synced_folder` の部分を `config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"` のように指定します（ソース配置場所は適宜読み替えてください）
        - バックスラッシュではなくスラッシュに変更する必要あり

## VM の起動

Vagrantfile の修正が終わったので、順次 `vagrant up` していきます。
コマンドプロンプトで以下のようにするだけです。

```console:コマンドプロンプト
cd /d D:\VirtualMachines\Vagrant\DockerTest\primary
vagrant up
cd ..\secondary
vagrant up
cd ..\mysql
vagrant up
```

# 環境構築

立ち上がったそれぞれの VM を構築していきます。
全て説明するのはかなり厳しいので、 Chef Infra Client により自動化しています。
また、 Chef Infra Client のインストール自体も手動ではなくシェルスクリプトで一括してやるようにしています。

## primary の場合

VM にログインするためにコマンドプロンプトを使います。

```console:コマンドプロンプト
cd /d D:\VirtualMachines\Vagrant\DockerTest\primary
vagrant ssh
```

VM にログイン出来たら、以下のコマンドを実行します。

```console:コマンドプロンプト
sudo /vagrant_data/provisioning_primary.sh
```

諸々インストールし、 Chef Infra Client で Docker をインストールし docker build で docker イメージを作成、その後 Docker Swarm の構築までやってくれちゃうので何が起こっているのか分かりづらいですかね。
docker のインストール自体は、公式の CentOS へのインストール手順に則って実施しています。
docker build は今回用意しているアプリケーション用に Dockerfile を用意してイメージを作成しています。
Docker Swarm は、管理サーバとなるホストでコマンドを実行して初期化をします。
今回では、 flask-primary のホストで以下のコマンドを実行します。

```console:コマンドプロンプト
docker swarm init --advertise-addr 192.168.33.10
```

Docker Swarm による管理対象に自分自身も入るので、以下のように primary がリーダーとして登録されていることが分かるかと思います。

```console:コマンドプロンプト
[vagrant@flask-primary ~]$ sudo docker node ls
ID                            HOSTNAME        STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
3gytnmzhmcw4y3gc9c68w9qdt *   flask-primary   Ready     Active         Leader           20.10.11
```

Docker Swarm の初期化ができたので、以下のコマンドを実行して Docker Swarm によるアプリケーション環境を立ち上げます。

```console:コマンドプロンプト
docker stack deploy --with-registry-auth -c /var/app/docker/docker-local.yml test
```

どのようにコンテナを起動するかを `docker-local.yml` で定義し、指定しています。
この YAML ファイルは、 Docker Compose のものと同一です。
Docker Swarm では、 Docker Compose を複数台のサーバーにまたいで管理できるコンテナオーケストレータ、というイメージです。（間違ってたらすみません）
なお、最後の `test` は、 Docker Swarm で立ち上げた際のサービス名です。
これがコンテナ名の先頭につきます。

## secondary の場合

primary と同じように、コマンドプロンプトで VM にログインします。

```console:コマンドプロンプト
cd /d D:\VirtualMachines\Vagrant\DockerTest\secondary
vagrant ssh
```

VM にログイン出来たら、以下のコマンドを実行します。

```bash:コマンドプロンプト
sudo /vagrant_data/provisioning_secondary.sh
```

すでに Docker Swarm のリーダーはいるので、こちらは join する側になります。
join するためには、リーダーでトークンを発行する必要があります。
そのため、 primary の方で以下のコマンドを実行します。

```bash:コマンドプロンプト
docker swarm join-token worker
```
実は primary でシェルスクリプトを実行した際の最後に実行しているので、コマンドが表示されていたのですが、同じものが上記コマンドで表示されるかと思います。
表示されるトークンは違うかと思いますが、以下のようなコマンドが表示されていると思います。

```bash:コマンドプロンプト
docker swarm join --token SWMTKN-1-68nxudsy4hq3p1ztiuvzuujzg8l20eur6b7q2k62jitnavd8cn-447u5cqaifqj9iq06o1g3r53f 192.168.33.10:2377
```

表示されているコマンドをそのままコピペして secondary で実行すると、 Docker Swarm に正常に参加できていれば自動で Docker コンテナが起動されます。（root ユーザにスイッチしていないと思うので、 `sudo` を先頭につける必要があります）
`docker ps` コマンドで確認できるかと思います。（こちらも `sudo` が必要です）
ただし、 Docker イメージはローカルにある必要があるため、シェルスクリプト内でこちらも `docker build` をしています。
Docker Hub のように外部リポジトリにイメージがあり、そこから `docker pull` できる形であれば、ローカルで `docker build` しておく必要はないはずですが、今回はリポジトリを作成していないため、ローカルのイメージを用いています。
なお、外部リポジトリから取得する場合は YAML ファイルでの image の指定を変更します。

## Docker Swarm の状態を確認

すでに実施済みですが、 primary の方で以下のコマンドを実行してみます。（`sudo` が必要なため `root` ユーザにスイッチしてます）

```bash:コマンドプロンプト
sudo su
docker node ls
```

すると、今度は secondary のサーバーが Docker Swarm の node として追加されているのが確認できます。
以下のようなイメージです。

```console:コマンドプロンプト
[root@flask-primary vagrant]# docker node ls
ID                            HOSTNAME          STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
3gytnmzhmcw4y3gc9c68w9qdt *   flask-primary     Ready     Active         Leader           20.10.11
rkxzyhty0iivmfdwqhq6z0o9s     flask-secondary   Ready     Active                          20.10.11
```

また、以下のコマンドでサービスの状態を確認可能です。

```bash:コマンドプロンプト
docker service ls
```

以下のような結果が返ってくると思います。

```console:コマンドプロンプト
[root@flask-primary vagrant]# docker service ls
ID             NAME         MODE         REPLICAS               IMAGE            PORTS
lv2hz1xinv8m   test_flask   replicated   4/4 (max 2 per node)   flask:latest
2psamemsshnh   test_nginx   global       2/2                    mynginx:latest   *:80->80/tcp
```

コンテナが片肺になっているかどうかなどを確認する時に使ったりします。

`docker stack deploy` したのにコンテナがうまく起動できないな、という時は、以下のコマンドで確認することができます。

```bash:コマンドプロンプト
docker stack ps test --no-trunc
```

これは、 `test` という名称で立ち上げた Docker Swarm のサービスのコンテナの状況を確認するコマンドです。
起動に失敗しているログが見えたりしますが、 Docker Swarm は起動できるまでコンテナの再起動を繰り返すので、多少のエラーは気にしなくても大丈夫です。

Docker Swarm で構築されたネットワークを確認するためには、まずネットワークの一覧を取得してみましょう。

```bash:コマンドプロンプト
docker network ls
```

デフォルトでは、 `サービス名_default` という名称でネットワークが作成されます。
今回の例では、 `test_default` となります。
では、このネットワークの詳細を確認しています。

```bash:コマンドプロンプト
docker network inspect test_default
```

ネットワークに所属するコンテナが見れます。
なお、 Docker Swarm で構築されたネットワークはデフォルトではアタッチ不可のため、 `docker stack deploy` した時に起動されたコンテナ以外はネットワークに入れません。
後でネットワークにコンテナを追加したい場合は、  docker-local.yml ではすでに指定しているのですが、 `attachable` の設定をする必要があります。
ただし、アタッチ可能にした場合、 `docker run` でネットワークを指定してコンテナを起動する時、アタッチした際にネットワークの再構築が行われるらしく、瞬断が発生することがあるようです。
そのため、アタッチ可能にすることは非推奨かもしれません。（最新のバージョンでは検証していないので分かりません）

今度は、コンテナの詳細情報を見てみましょう。
以下のようなコマンドで確認可能です。

```bash:コマンドプロンプト
docker service inspect test_flask
```

簡略表示したい場合は、 `--pretty` オプションを付けます。

コンテナのログを見る場合は、以下のコマンドで確認します。

```bash:コマンドプロンプト
docker service logs -f test_flask
```

docker-local.yml で log-driver の指定をすれば、上記コマンドでなくてもログを指定した方法で転送可能ですが、ここでは取り扱いません。
よく使われるのは、 fluentd とかですかね。

まだ MySQL の VM を構築してないので次に進みます。

## mysql-server の場合

これまでと同様に、コマンドプロンプトで VM にログインしてシェルスクリプトを実行します。

```console:コマンドプロンプト
cd /d D:\VirtualMachines\Vagrant\DockerTest\secondary
vagrant ssh
```

```bash:コマンドプロンプト
sudo /vagrant_data/provisioning_db.sh
```

MySQL をインストールし、 `mydb` という DB を構築し、あとはソース上にある `init.sql` を実行するだけです。
ユーザー追加とテーブル追加をするだけですね。

これで構築完了です。

# 動作確認

REST API なので、動作確認をしてみましょう。
mysql-server で、 curl コマンドで確認可能です。
例えば、以下のコマンドをそれぞれ実行してみてください。

```bash:コマンドプロンプト
curl http://192.168.33.10
curl http://192.168.33.10/v1/groups
curl http://192.168.33.10/v1/users
```

JSON 形式でデータが返ってきたと思います。
チープな機能しか実装していないため、ユーザーとグループの CRUD しかできません。
さらに、ユーザーとグループは n:1 で紐づくようなテーブル構成になっていますが、 REST API の戻り値では特にそこを意識していません。
そこまでは手が回りませんでした……

REST API は、以下のように操作します。

```bash:コマンドプロンプト
# 全グループを取得
curl http://192.168.33.10/v1/groups
# 指定したグループIDのデータを取得
curl http://192.168.33.10/v1/groups/1
# グループを追加
curl http://192.168.33.10/v1/groups -XPUT -H"content-type:application/json" -d'{"group_name": "group1"}'
# グループを更新
curl http://192.168.33.10/v1/groups/1 -XPOST -H"content-type:application/json" -d'{"group_name": "test_group1"}'
# グループを削除
curl http://192.168.33.10/v1/groups/1 -XDELETE

# 全ユーザーを取得
curl http://192.168.33.10/v1/users
# 指定したユーザーIDのデータを取得
curl http://192.168.33.10/v1/users/1
# ユーザーを追加
curl http://192.168.33.10/v1/users -XPUT -H"content-type:application/json" -d'{"user_name": "user1", "group_id": 1}'
# ユーザーを更新
curl http://192.168.33.10/v1/users/1 -XPOST -H"content-type:application/json" -d'{"user_name": "test_user1"}'
# ユーザーを削除
curl http://192.168.33.10/v1/users/1 -XDELETE
```

それぞれ動作確認してみてください。

# 終わりに

動作確認できたでしょうか。
ちなみに、 primary を `vagrant halt` で落としても、 `192.168.33.11` の方で動作はできます。
primary を立ち上げなおすと、元通りになるみたいです。
なお、 secondary を worker ではなく manager として join すると、 primary を停止して立ち上げなおすとリーダーと manager が入れ替わりました。
この挙動の違いがどう影響するのかは調べていないので、余裕のある時にでも更新します……

# 余談

ようやく Docker Swarm まで記事にできました。
実際にこれを試してみようと思われた方、ありがとうございます。
記事にした甲斐がありました。

ただ、職場で試される方は要注意です。
プロキシ環境下で試される方は、 Docker のプロキシ設定が必要です。
以下のディレクトリを作成し、コンフィグファイルを配置する必要があります。

```
/etc/systemd/system/docker.service.d
```

```proxy.conf
[Service]
Environment="HTTP_PROXY={proxy url}"
```

ファイル名は適当、 url は適宜設定してください。
また、 docker build 時にも proxy 設定が必要です。

```bash
docker build --build-arg HTTP_PROXY={proxy url} --build-arg HTTPS_PROXY={proxy url} -t hoge .
```

シェルスクリプトを書き換えて実行しましょう。

また、場合によっては SELinux や firewalld も無効にしないといけないかも。
今回使用した bento の box は、 firewalld があらかじめ無効になっているためファイアウォールを意識せずに構築できています。

ということで、ひとまず Docker の記事はこれにて終了です！
お疲れさまでした👍