DockerでEMQXクラスタを構築してみた

# はじめに

MQTTブローカー「[EMQX](https://www.emqx.com/ja)」は、スケーラビリティと高可用性を重視した設計になっており、クラスタリング機能を備えています。

本記事では、**Docker環境上でEMQXのクラスタリングを試す手順**を紹介します。

### 参考
https://docs.emqx.com/en/emqx/latest/deploy/cluster/introduction.html

https://docs.emqx.com/en/emqx/latest/design/clustering.html

# 検証環境

今回使用したVM（Proxmox上）の構成は以下の通りです：

| 項目 | 内容 |
|------|------|
| RAM | 16.00 GiB |
| CPU | 8コア（1ソケット、8コア） [x86-64-v2-AES] |
| OS | Ubuntu 24.04.2 LTS |

この環境上でDockerとDocker Composeを使用してEMQXクラスタを構築します。

# 使用ツール・バージョン

- Ubuntu Server 24.04.2  
- Docker 28.0.4
- Docker Compose v2.34.0
- EMQX 5.8.6（今回はオープンソース版を使用）


# 構成概要

以下のような2ノード構成でEMQXクラスタを構築します。

- `emqx2-1`（EMQXノード）  
- `emqx2-2`（EMQXノード）  

同一ネットワーク内で2ノードが相互にクラスタリングされ、どのノードに接続してもメッセージが共有される構成です。

---

### 0. UbuntuにDocker Engineをインストールする
<details><summary>以下は公式ドキュメントより引用</summary>

URL：https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

1. Set up Docker's apt repository.
    ```bash
    # Add Docker's official GPG key:
    sudo apt-get update
    sudo apt-get install ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    
    # Add the repository to Apt sources:
    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
      $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
      sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    ```

2. Install the Docker packages.（Latest）
    ```bash
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    ```

3. Verify that the installation is successful by running the hello-world image:
    ```
    sudo docker run hello-world
    ```
    This command downloads a test image and runs it in a container. When the container runs, it prints a confirmation message and exits.

4. ユーザーをdockerグループに追加する
    ```bash
    sudo gpasswd -a hogehoge docker
    ```

</details>

### 1. Docker Compose構成

EMQXの公式Dockerイメージ（[emqx/emqx:latest](https://hub.docker.com/r/emqx/emqx)）を使用し、クラスタをDocker Composeで構築します。

以下は、2ノード（`emqx2-1`, `emqx2-2`）間でクラスタリングを行う基本構成例です。必要に応じてノード数を拡張してください。

```docker-compose.yaml
version: '3'

services:
  emqx1:
    container_name: emqx2-1 # ※公式ドキュメントには書かれてないが、識別しやすくするために明示的に指定
    image: emqx/emqx:latest
    environment:
    - "EMQX_NAME=emqx" # ノード名のプレフィックス（クラスタリングに使用）
    - "EMQX_HOST=node1.emqx.io" # クラスタ内で一意のホスト名
    - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static"
    - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io, emqx@node2.emqx.io]"
    networks:
      emqx-bridge:
        aliases:
        - node1.emqx.io

  emqx2:
    container_name: emqx2-2 # ※こちらも同様に補助的に付与
    image: emqx/emqx:latest
    environment:
    - "EMQX_NAME=emqx"
    - "EMQX_HOST=node2.emqx.io"
    - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static"
    - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io, emqx@node2.emqx.io]"
    networks:
      emqx-bridge:
        aliases:
        - node2.emqx.io

networks:
  emqx-bridge:
    driver: bridge
```

### 2. 2つのノードを起動

先ほど作成した`docker-compose.yml`を使用して、EMQXクラスタノード（`emqx2-1`, `emqx2-2`）を起動します。

```bash
docker compose -p my_emqx_broker2 up -d
```
:::note info
公式ドキュメントで使用されている`docker-compose`（ハイフンあり）は旧バージョンのコマンド形式であり、Docker Compose v2以降では`docker compose`（ハイフンなし）が推奨されています。
:::

正常に起動しているかどうかを確認します。
```bash
$ docker ps
CONTAINER ID   IMAGE              COMMAND                  CREATED        STATUS        PORTS                                                              NAMES
bd0c84955d0f   emqx/emqx:latest   "/usr/bin/docker-ent…"   33 hours ago   Up 33 hours   1883/tcp, 4370/tcp, 5369/tcp, 8083-8084/tcp, 8883/tcp, 18083/tcp   emqx2-1
8de033b356b6   emqx/emqx:latest   "/usr/bin/docker-ent…"   33 hours ago   Up 33 hours   1883/tcp, 4370/tcp, 5369/tcp, 8083-8084/tcp, 8883/tcp, 18083/tcp   emqx2-2
```

いずれかのノード内で`emqx_ctl`コマンドを実行し、クラスタリングが成功しているかを確認します。
```bash
$ docker exec -it emqx2-1 sh -c "emqx_ctl cluster status"
Cluster status: #{running_nodes => ['emqx@node1.emqx.io','emqx@node2.emqx.io'],
                  stopped_nodes => []}
```
:::note info
`running_nodes`に2つのノードが表示されていれば、クラスタが正常に構成されている状態です。
:::

### 3. MQTTクライアントを使用して動作確認

クラスタ構成が正しく動作しているか確認するために、MQTTクライアントとして[efrecon/mqtt-client](https://hub.docker.com/r/efrecon/mqtt-client)を使用します。このコンテナイメージには`mosquitto_sub`および`mosquitto_pub`が含まれており、簡単にMQTTメッセージの送受信を試すことができます。

:::note warn
#### Dockerネットワークの指定
今回はDocker Composeで定義されたカスタムネットワーク（`emqx-bridge`）内でコンテナ同士が通信しているため、`mqtt-client`も同じネットワークに所属させる必要があります。

このため、クライアント実行時には`--network`オプションで`my_emqx_broker2_emqx-bridge`を明示的に指定しています。（`my_emqx_broker2`は`docker compose`コマンドで指定したプロジェクト名）
:::

- Subscriberを起動（`node2.emqx.io`に接続）
    まず、`node2.emqx.io`をホストとするSubscriberを起動し、`emqx/test`トピックを購読します。
    ```bash
    $ docker run -it --rm \
        --name sub \
        --network my_emqx_broker2_emqx-bridge \
        efrecon/mqtt-client \
        sub -h node2.emqx.io -t "emqx/test"
    ```
    
- Publisherを起動（`node1.emqx.io`に接続）
別のターミナルから、`node1.emqx.io`に接続し、同じトピックにメッセージを送信します。
    ```bash
    $ docker run --rm \
        --name pub \
        --network my_emqx_broker2_emqx-bridge \
        efrecon/mqtt-client \
        pub -h node1.emqx.io -t "emqx/test" -m "Hello from publisher"
    ```

- メッセージが届いているか確認
    Subscriberの出力に以下のようなメッセージが表示されていれば、クラスタリングが正しく機能しています。
    ```bash
    Hello from publisher
    ```

このように、別ノード間でメッセージの送受信ができていることから、EMQXクラスタ構成が正常に機能していることが確認できます。

### 通信の流れをパケットキャプチャで確認（おまけ）
Wiresharkを使用して、EMQXクラスタ構成におけるメッセージの通信経路をパケットキャプチャで可視化しました。

以下はその一連の流れです。（ssh越しにDockerのネットワークをキャプチャしています。※この方法については、今後また紹介予定です）

今回のネットワーク構成は以下のようになります。

![network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4030327/a4f445fd-69ec-4640-9e27-7e06692805ee.png)


1. Publisherから`node1.emqx.io`へのメッセージ送信
    - 送信元IP：172.20.0.5（Publishier）
    - 宛先IP：172.20.0.3（`node1.emqx.io`）
    
    MQTT Publishパケット：
     - TCPポート: 1883
     - Topic: emqx/test
     - Payload: "Hello from publisher"

    ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4030327/a36d21cb-b14f-414a-bdfc-fca00761d7b3.png)

2. `node1.emqx.io`から`node2.emqx.io`へのクラスタ間転送
    - EMQXノード間のクラスタ通信には、デフォルトでTCPポート`5369`（Docker環境の場合）が使用されます。
        - 参考：[How EMQX Nodes Communicate](https://docs.emqx.com/en/emqx/latest/design/clustering.html#how-emqx-nodes-communicat)
    - 送信元IP：172.20.0.3（`node1.emqx.io`）
    - 宛先IP：172.20.0.2（`node2.emqx.io`）

    ![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4030327/4f0ab727-381d-4aa8-bb6c-791feb49043c.png)

3. `node2.emqx.io`からSubscriberにメッセージを送信
    - 送信元IP：172.20.0.2（node2.emqx.io）
    - 宛先IP：172.20.0.4（Subscriber）


    ![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4030327/c05aaad1-a5a1-436e-aee8-c4177279dcdc.png)

# まとめ
今回は、Docker Composeを用いたEMQXクラスタ構築および動作確認までの流れを解説しました。
今後はEMQXクラスタにおけるルーティングの仕組みを調査します。