リモートのDocker Swarmをラップトップから操作する

今更ながらDocker Swarmに入門することとなりました。普段K8sに慣れていたのでK8sと同じようにラップトップ側でクラスタの操作をしたいと思いました。調べたらDockerのAPIを外部に公開することで可能なようです。ただ、おもむろに公開するのはセキュリティ的にもよろしくないのでTLSを設定して安全にしたいと思います。

基本的に公式ドキュメントの[ここ](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket)を参考にして進めていきます。

# 環境説明

AWS上のEC2にDockerをインストールしています。ホストは全部で5台ありSwarmクラスタを組んでいます。5台中3台がmanagerで残り2台がworkerです。各ホストのスペックは以下の通りです。各ホストはパブリックサブネットに配置しています。

- インスタンスタイプ: t2.medium
- OS: t2.medium
- docker: docker-ce 20.10.7

クライントは私のラップトップです。各ホストには私のラップトップからのインバウンドを許可するセキュリティグループルールを設定してあります。

今回はmanager3台に設定を追加して外部から操作可能にします。

# 手順

## 証明書準備

以降の手順は**managerの内どれか1台**で実施します。rootで作業する想定です。

TLSで通信したいためCAの認証局を作ります。

``` sh
$ openssl genrsa -aes256 -out ca-key.pem 4096
# pass phraseは適当な4文字以上の文字列を入れる。

$ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# CNはDockerホストのパブリックDNS名などクライアントから名前解決可能なホスト名にする。
```

サーバーの秘密鍵とCSRを作成します。

``` sh
$ openssl genrsa -out server-key.pem 4096
$ openssl req -subj "/CN=<Dockerホスト名①>" -sha256 -new -key server-key.pem -out server.csr
# CNはmanagerの内どれか一台代表でOK。後ほどsubjectAltNameで他のmanagerも設定します。
```

サーバー用の拡張設定をファイルに保存します。ここで全managerのパブリックDNS名やパブリックIPアドレスの設定を追加しておきます。

``` sh
$ echo subjectAltName = DNS:<Dockerホスト①>,DNS:<Dockerホスト②>,DNS:<Dockerホスト③>,IP:<Dockerホスト①>,IP:<Dockerホスト②>,IP:<Dockerホスト③> >> extfile.cnf
$ echo extendedKeyUsage = serverAuth >> extfile.cnf
```

サーバー証明書を作成します。

``` sh
$ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf
```

クライアントの秘密鍵とCSRを作成します。

``` sh
$ openssl genrsa -out key.pem 4096
$ openssl req -subj '/CN=client' -new -key key.pem -out client.csr
```

クライアント用の拡張設定をファイルに保存します。

``` sh
$ echo extendedKeyUsage = clientAuth > extfile-client.cnf
```

クライアント証明書を作成します。

``` sh
$ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out cert.pem -extfile extfile-client.cnf
```

パーミッションを整えておきます。

``` sh
$ chmod -v 0400 ca-key.pem key.pem server-key.pem
$ chmod -v 0444 ca.pem server-cert.pem cert.pem
```

作成した`ca.pem`、`server-cert.pem`、`server-key.pem`を他のmanagerにもコピーします。（各種ファイルをcatして中身をコピペでもいい。）

## dockerd設定

以降の手順は**すべてのmanager**で実施します。rootで作業する想定です。

ここから[参考手順](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket)にないオリジナル手順。dockerdをTLS有効にして起動するためdaemon.jsonを設定します。設定は[ここ](https://docs.docker.jp/config/daemon/daemon.html)を参考にします。

まず各種ファイルをディレクトリに配置します。

``` sh
$ mkdir /var/docker
$ mv ca.pem /var/docker
$ mv server-cert.pem /var/docker
$ mv server-key.pem /var/docker
```

daemon.jsonを作成します。（すでにある場合は編集にします。）

``` sh
$ cat <<EOF > /etc/docker/daemon.json
{
  "tls": true,
  "tlscacert": "/var/docker/ca.pem",
  "tlscert": "/var/docker/server-cert.pem",
  "tlskey": "/var/docker/server-key.pem",
  "hosts": ["tcp://0.0.0.0:2376","unix:///var/run/docker.sock"]
}
EOF
```

また、上記のようにdaemon.jsonでhostsを指定する場合は/etc/systemd/system/docker.service.d/docker.confを作成しておく必要があります。[ネタ元](https://docs.docker.com/config/daemon/#troubleshoot-the-daemon)

``` sh
$ mkdir /etc/systemd/system/docker.service.d/
$ cat <<EOF > /etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
EOF
```

上記ファイルを作成したらデーモンリロードしてdockerを起動しなおします。

``` sh
$ systemctl daemon-reload
$ systemctl restart docker
```

## クライアント設定

以降の手順は**クライアント**で実施します。

`ca.pem`、`cert.pem`、`key.pem`をクライントにコピーします。やり方はお好きな方法でどうぞ。（各種ファイルをcatして中身をクライアントにコピペでもいい。）

資材をクライアントに持って行ったら以下コマンドでリモートのDockerホストに接続できるか確認します。クライアント→Dockerホストでport:2376のインバウンドが許可されていることに注意してください。

``` sh
$ docker --tlsverify \
    --tlscacert=ca.pem \
    --tlscert=cert.pem \
    --tlskey=key.pem \
    -H=<Dockerホスト①IP>:2376 node ls
```

上記接続に問題なければ以下コマンドでコンテキストを作成しておきます。名前やhostを変えて他のホストのコンテキストも作っておきます。また、ホストの指定はパブリックDNS名でもよいです。

``` sh
$ docker context create manager0 --description "remote manager0" --docker "host=tcp://<Dockerホスト①IP>:2376,ca=<ca.pemフルパス>,cert=<cert.pemフルパス>,key=<key.pemフルパス>"
$ docker context create manager1 --description "remote manager1" --docker "host=tcp://<Dockerホスト②IP>:2376,ca=<ca.pemフルパス>,cert=<cert.pemフルパス>,key=<key.pemフルパス>"
$ docker context create manager2 --description "remote manager2" --docker "host=tcp://<Dockerホスト③IP>:2376,ca=<ca.pemフルパス>,cert=<cert.pemフルパス>,key=<key.pemフルパス>"
```

使用するコンテキストを切り替えます。

``` sh
$ docker context use manager0
```

オプションを指定しなくてもリモートのDockerホストにつながることを確認します。

``` sh
$ docker node ls
```

# 動作確認

簡単なcomposeファイルを作成してスタックがデプロイできることを確認します。

``` sh
$ cat <<EOF > test.yaml
version: "3.9"

services:
  test:
    image: nginx
    deploy:
      replicas: 1
EOF
```

スタックをデプロイします。

``` sh
$ docker stack deploy -c test.yaml test
```

スタックがデプロイできたことを確認します。

``` sh
$ docker stack ls
$ docker stack services test
$ docker stack ps test
```

別managerのコンテキストに切り替えても見えることを確認します。

``` sh
$ docker context use manager1
$ docker node ls
$ docker stack ps test
```

# あとがき

マネージャの前段にLB立ててやる方がカッコいいだろうとなと思いつつ、とりあえず目的は達成できたのでこれで我慢する。