Portainerで複数ホストのコンテナを一元管理してPortainerはいいぞと言う

この記事は[Docker Advent Calendar 2020](https://qiita.com/advent-calendar/2020/docker)の12日目の記事として書かれました。

## 結論
Portainerはいいぞ。

## 環境
* Docker CE 19.03.13
* Docker Compose 1.27.4
* Portainer 2.0.0


## Portainerとは
DockerコンテナやイメージなどをGUIで管理できるツールです。
小洒落たUIとシンプルな操作性で、入門者から上級者まで幅広い層にオススメできるツールです。
![portainer00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/d0c90335-25e1-d49a-31c5-a6138eb9683c.png)
公式のデモ環境があるので、一度触ってみてください。
http://demo.portainer.io/
ID:admin
PW:tryportainer
(参考：[公式Githubリポジトリ](https://github.com/portainer/portainer))

## 基本的な使い方
まずはPotainerを使ってみましょう。
Portainer自体がDockerイメージとして提供されているため、以下のようにコンテナを起動するだけでOKです。

```
$ docker run \
    --detach \
    --publish 9000:9000 \
    --publish 8000:8000 \
    --name portainer \
    --restart always \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    --volume portainer_data:/data \
    portainer/portainer-ce
```

`portainer_data` は登録した情報(ログインユーザの情報など)を永続化するためのボリュームです。

`http://<ホスト名>:9000` にアクセスすると、以下のようにパスワードの設定を求められます。適当な値を設定してください。
![startup01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/ccf22799-00c4-1cb0-8d02-378af2b952f7.png)
続いて管理対象の環境を選択します。[Docker] を選び [Connect] を押します。
問題なければHome画面に`local`なるエンドポイントが表示されます。

`local` をクリックすると `Containers` や `Images` 等のリンクが表示されます。一旦 `Containers` を確認すると、以下のようにPortainerのコンテナが正常に動作している様子を確認できます。
![startup04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/c23f818d-1bd5-02e6-851f-79e7b2a1dfe5.png)
コンテナを追加すると自動的にPortainerの管理対象に入ります。適当にnginxを立ち上げましょう。

```
$ docker run -dit -p 8080:80 --name nginx nginx
```

特にエラーが出なければ、コンテナ一覧にnginxがあることが確認できるでしょう。

各コンテナの画面ではlogsでログ確認、inspectでコンテナの情報確認、statsでリソース使用量等の確認ができます。また、コンテナ起動時に`--interactive`, `--tty` (`-it`)オプションを設定していればconsoleでコンテナ内部に対して操作を行うことができます。試しに先ほど起動したnginxに対してconsoleでアクセスします。

コンテナ一覧から [nginx] -> [Console] とアクセスし、 [Connect] を押すと以下のようにコンテナ内に入れます。とてもお手軽です。
![nginx02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/b8dddc8e-1eab-4a83-621e-be9470b1cc13.png)
基本的な使い方を確認できたところで、一旦Portainerを削除します。

```
$ docker stop portainer && docker rm portainer
```

さて、ここまででPortainerで単一ホストのコンテナを管理する方法が分かりました。ただ、実際にはDockerホストは複数のノードにまたがっていることもあるでしょう。

この記事ではDocker Swarmで作成されたクラスタを単一のPortainerで管理する方法について説明します。

## Docker Swarmで構築したクラスタのコンテナを一元管理する
### 1. Swarmの準備
今回は図のようなクラスタを構築しました。
![cluster.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/3b17468e-8692-3cb4-fea6-3e725d9d315d.png)

| 名称    | 役割    | 説明                    |
| :-------: | :-------: | :----------------------- |
| ip-172-31-36-96 | MANAGER | Swarmのマネージャノード |
| ip-172-31-32-220   | WORKER  | Swarmのワーカーノード   |
| ip-172-31-40-65   | WORKER  | Swarmのワーカーノード   |

manager1台、worker2台という構成です。

Dockerホストは`docker-machine`で用意するもよし、実際に何らかのインスタンスを立てるもよしです。
今回はmanagerもworkerもAWSで雑にインスタンスを立ててDockerとDocker Composeを入れました。1.27.4の部分は[公式ガイド](https://docs.docker.com/compose/install/)を参考に適宜最新版に置き換えてください。

```
$ sudo yum -y install docker
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
```

**(注意)** クラスタ内の各ノード間で以下のポートにアクセスできるよう設定する必要があります。クラウドサービスでホストを立てたりすると恐らくデフォルトでは閉じているはずなので、各ノードが相互に以下のポートを利用できるよう設定します。

| ポート番号 | プロトコル | 用途                         |
| :--------: | :--------: | ---------------------------- |
|    2377    |    TCP     | クラスタ管理通信             |
|    7946    |    TCP     | ノード間通信                 |
|    7946    |    UDP     | ノード間通信                 |
|    4789    |    UDP     | オーバーレイネットワーク通信 |

ネットワークの設定が完了したら、各ホストをSwarmのメンバーにします。

* Swarmの作成＠MANAGER

```
$ docker swarm init --advertise-addr <自身のIPアドレス>
```

<自身のIPアドレス>にはSwarmの他ノードから識別可能なIPアドレスを設定します。今回の例では172.31.36.96です。

* Workerを参加させるためのトークンを発行＠MANAGER

```
$ docker swarm join-token worker -q
```

`SWMTKN` から始まるトークンが表示されます。 -q はトークンのみ表示するというオプションで、あってもなくてもいいです。(コピペが楽なので付けています)

* 各WorkerをSwarmに参加させる＠WORKER

```
$ docker swarm join --token <トークン> <managerのIPアドレス>
```

<トークン>は先ほど作成した`SWMTKN` から始まるものです。<managerのIPアドレス>はSwarm作成時のmanagerの<自身のIPアドレス>と同じものを指定してください。

* 設定の確認＠MANAGER
Swarmを正常に設定できていれば、ノード一覧に3つのホストが表示されます。

```
$ docker node ls
ID                            HOSTNAME                                           STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
v6zghq4r796dbg2ixow620um9     ip-172-31-32-220.ap-northeast-1.compute.internal   Ready               Active                                  19.03.13-ce
7gbjabs2eg0rqjhvkkdqwr4dq *   ip-172-31-36-96.ap-northeast-1.compute.internal    Ready               Active              Leader              19.03.13-ce
xwl862sqjb5n2hs5z6nxmdz99     ip-172-31-40-65.ap-northeast-1.compute.internal    Ready               Active                                  19.03.13-ce
```

これでクラスタが完成しました。

### 2.Stackのデプロイ
StackはServiceの集合です。大雑把に捉えるとDocker Swarmはサーバを束ねる技術でDocker Stackはアプリケーションを束ねる技術です。

公式でPortainerのStackを構築するためのymlファイルが提供されているので、curlでダウンロードしてデプロイします。

```
$ curl -L https://downloads.portainer.io/portainer-agent-stack.yml -o portainer-agent-stack.yml
$ docker stack deploy --compose-file=portainer-agent-stack.yml portainer
```

デプロイが完了した後Portainerにアクセスすると、以下のように4つのコンテナが表示されます。Hostカラムから、3つの異なるホストにあるコンテナが1つのPortainerで管理できていることが分かります。

![portainer01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/ffcc05d1-422e-bd36-3736-0411a9a405f9.png)
Stackとは別に各ノードでnginxを立ちあげてみましょう。

```
$ docker run -dit -p 8080:80 --name nginx nginx
```
![portainer02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/b5fb2b69-e1c4-ee15-3c61-171dd6ce174c.png)
すべてのnginxがきちんと表示されます。`--interactive`, `--tty`オプションをつけて起動したので、Consoleを利用できます。当然、Portainerが所属しているノードとは別のノードのnginxのConsoleにもアクセスできます。
これで複数ノードのコンテナを一元管理できるようになったことを確認できました。

今回デプロイしたStackの内容を見てみましょう。

``` portainer-agent-stack.yml
version: '3.2'

services:
  agent:
    image: portainer/agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer-ce
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9000:9000"
      - "8000:8000"
    volumes:
      - portainer_data:/data
    networks:
      - agent_network
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

networks:
  agent_network:
    driver: overlay
    attachable: true

volumes:
  portainer_data:
```

2つのサービス`portainer`と`agent`を確認できます。`portainer`はGUIアプリケーションそのものです。`agent`は自ノードとPortainerの橋渡し役を担っています。
注目するべきは`agent`で定義されている`mode: global`でしょう。これはクラスタの各ノードに該当サービスをデプロイするという指定です。各ノードにagentがいるおかげで、クラスタ内の全ノードのコンテナの情報を管理できるようになっています。

`networks`は両サービスが利用するネットワークを、`volumes`はPortainerのデータを永続化するボリュームを定義しています。どちらもデプロイ時になければ勝手に作成されます。

### 3. Stackの更新

ここまでで複数ホストのコンテナの一元管理を実現できました。
補足としてnginxをStackに組み込んでデプロイした時の挙動を紹介します。
一旦各ノードのnginxコンテナは削除し、サービスにnginxを突っ込んで再度デプロイします。

```　portainer-agent-stack.yml
version: '3.2'

services:
  agent:
    image: portainer/agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer-ce
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9000:9000"
      - "8000:8000"
    volumes:
      - portainer_data:/data
    networks:
      - agent_network
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
        
  web:
    image: nginx
    tty: true
    ports:
      - "80:80"
    networks:
      - agent_network
    deploy:
      mode: replicated
      replicas: 3

networks:
  agent_network:
    driver: overlay
    attachable: true

volumes:
  portainer_data:
```

```
$ docker stack deploy --compose-file=portainer-agent-stack.yml portainer
```

Portainerを確認すると、きちんとサービス`web`がStackに組み込まれた状態でデプロイされていることが分かります。
![portainer03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/24715822-4674-d952-2d29-6709a6448dc4.png)

## 余談
Portainerはシンプルかつ高機能で、色々なことができます。いくつか「おー」と思ったものを紹介します。

* テンプレートからアプリケーションの立上げ
![portainer04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/49bb57ff-160f-2d2f-a82e-f4d736a35bd1.png)
WordPressとかGitlabみたいなちょっと面倒なやつらもボタンポチポチで起動できます。

* Stackのデプロイ
![portainer05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/2f921a6a-50c0-5437-15e6-b8c11263fe1c.png)
ymlファイルをベタ書きするもよし、ymlファイルをアップロードするもよしです。

* LDAP / OAuth認証
![portainer06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199887/b4cf0f27-2047-8fe5-c194-967b19c18bdf.png)
既存の認証情報を利用することができます。

## おわりに
色々と機能を紹介したものの、正直に言ってローカルホストのコンテナの管理機能だけでも相当にパワフルです。
特にConsole機能は頻繁に使います。CUIで操作していると「あれ、今ホストだっけゲストだっけ？」となることが度々あるので。。。

バージョン2.0からKubernetesに対応したらしいのでそちらも今後触ってみたいですね。
Kubernetes何もわからないので完全に理解したらまた関連記事を書こうと思います。

Portainerはいいぞ。

## 参考
* [portainer/portainer](https://github.com/portainer/portainer)  
Portainerのリポジトリです。
* [Portainer Deployment](https://portainer.readthedocs.io/en/stable/deployment.html)  
本記事は"Inside a Swarm cluster"の内容を大いに参考にしました。
* [Docker Swarm 導入チュートリアル](https://docs.docker.jp/engine/swarm/swarm-tutorial/toc.html)  
Docker Swarmによるクラスタ構築の基礎です。