docker swarm上で展開されるコンテナに任意のIPアドレスを割り振りたい

# はじめに
dockerは、`run -ip X.X.X.X`や`create -ip X.X.X.X`でIPを指定すると、コンテナに任意のIPアドレスを割り振ることができます。
これと同様に、**docker swarmで管理されるコンテナに関しても、任意のIPアドレスを割り振りたい**と考えました。しかし、これが想像以上に茨の道でした。
結果から書いてしまうと、swarmの管理下でIPアドレスを指定してコンテナを展開することは**できませんでした。**
この結論に至るまでに調べた内容をまとめておきます。


# 基本知識について
dockerそのものやdocker swarmそのものに関する説明は本記事では割愛させていただきます。基本的な説明は[こちら](http://docs.docker.jp/index.html)などを参考にしてください。

# 環境
調査・検証に際し、実際にdockerコンテナのデプロイやswarmクラスタの構築を行っています。
バージョンはそれぞれ以下の通りです。

* OS : CentOS 7.3
* docker : 18.09.1
* docker-compose : 1.24.0  
    * docker-py : 3.7.2
    * CPython : 2.7.5
    * OpenSSL : 1.0.2k-fips

また、各種リファレンスも記事執筆時（2019/6/19）時点のものを参照しています。

# 本編
## runとservice
前述の通り、`docker run`はオプションとして`-ip`を指定することができ、任意のIPアドレスをコンテナに付与できます。単一ホスト上で実行する場合はこれで十分です。
ですが、docker swarmの管理下でコンテナを展開する場合は`docker service`というコマンドを実行する必要があります。`docker run`を使用することはできません。

サービス（service）はdocker swarmにおけるアプリケーションの実行単位です。一つのイメージから複数のコンテナをレプリカ（replica）展開することができるため、「あるイメージで表現されるアプリケーションを冗長化したい」「アプリケーションの運用状況に応じてスケールしたい」といった要求に対応できます。ざっくり言えば「複数のrunの集合がservice」です。

しかしながら、**serviceにはrunの--ipに相当するコマンドはありません。**
`docker service`からサービスを生成してswarmの管理下にて展開されたそれぞれのコンテナには、使用するネットワークに割り当てられているサブネットから動的にIPアドレスが割り振られてしまいます。

## serviceとstack
ここで「複数のrunの集合がservice」であったように、「複数のserviceの集合」に相当する概念が存在します。それがスタック（stack）です。
スタックは複数のサービスを一元的に管理します。それぞれのサービスは異なるイメージを使用することができますし、サービス間の依存関係も定義することができます。
そして、一般的に、stackはcompose fileと呼ばれるyml形式のファイルでその内容を記述するのですが、ここには**IPアドレスを設定するオプションが存在します。**
[公式のリファレンス](https://docs.docker.com/compose/compose-file/#ipv4_address-ipv6_address)には以下のようなcompose fileが紹介されています。

```yml:docker-compose.yml
version: "3.7"

services:
  app:
    image: nginx:alpine
    networks:
      app_net:
        ipv4_address: 172.16.238.10
        ipv6_address: 2001:3984:3989::10

networks:
  app_net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"
        - subnet: "2001:3984:3989::/64"
```

指定する際には、利用したいネットワークを宣言し、サービス内でネットワークに指定したサブネットに対応するIPを指定します。
この例では192.168.238.0/24をサブネットとするapp-netという名のoverlayネットワークに対して172.168.238.10というIPアドレスを指定しています。（なお、IPv4だけでなくIPv6も指定しています。）

このように定義したcompose fileからスタックを開始しサービスを作成することで、コンテナは任意のIPが割り振られた状態になります。
**...と思っていました。**

## ipv4_addressが反映されない
公式に従った書き方をしても、普通にserviceを作成したときと同じように、**コンテナには動的にIPアドレスが割り振られてしまい、指定したIPアドレスは割り当てられませんでした。**
また、特にエラーメッセージが吐かれることもなかったため、正常な動作をした上での挙動と考えられます。

これを踏まえて調査してみたところ、以下のようなissueが見つかりました。

[#31860](https://github.com/moby/moby/issues/31860) （他にも[#25303](https://github.com/moby/moby/issues/25303)、[#24170](https://github.com/moby/moby/issues/24170)、[#29816](https://github.com/moby/moby/issues/29816)など）

簡単に要約すれば**「swamrモードではipアドレスの指定には対応していない」**とのことです。
少し古いissueではありますが、他に調べても新しい情報は見当たらなかったので現在でも信ぴょう性のある情報ではないかと考えます。

こうしたissueや実際の挙動を総合して考えると、**記法としてIPアドレスを任意に指定するフォーマットは用意されているが、実際に指定できるわけではない**というのが現状だと結論づけれそうです。


## 場当たり的な対処法
無理矢理にでも任意のIPアドレスを設定したいときは`docker network disconnect`と`docker network connect`が使えます。

`docker network disconnect`はコンテナをネットワークから切り離すコマンドです。逆に、`docker network connect`はコンテナにネットワークを接続するコマンドです。そしてこの`docker network connect`にはIPを指定する機能があります。故に、

サービスを起動してコンテナを展開する→`docker network disconnect`で、コンテナを一旦networkから切断する→`docker network connect`でIPを指定してコンテナをnetworkに接続する

という手順を踏むことでコンテナに任意のIPアドレスを指定することができます。例として、app-netに接続されたfooコンテナについて、ipアドレスを192.16.238.5から192.16.235.10に変換すると、以下のようになります。

```terminal
[user@worker ~]$ docker inspect --format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' foo
192.16.238.5
[user@worker ~]$ docket network disconnect app-net foo
[user@worker ~]$ docker inspect --format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' foo

[user@worker ~]$ docker network connect --ip 192.168.238.10 app-net foo
[user@worker ~]$ docker inspect --format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' foo
192.16.238.10
```

しかしながら、これらのコマンドはdocker swarmの機能ではなく、dockerの機能です。そのため、**swarmのmasterから操作することはできません。**実際にコンテナが展開されているホスト上で実行するする必要があります。
swarmの管理下に置いておく意味が全くない操作となってしまっているわけですね…

# 最後に
できそうなのにできない、歯がゆい結果となってしまいました。
「もっと賢くIPアドレスを指定する方法がある」「compose fileからでもきちんとIPを指定できる」といった指摘があればコメントをいただけますと幸いです。