Kubernetes （Azure AKS）上にVerneMQでMQTT over TLSクラスタを構成する

# はじめに

IoTという言葉がバズってからもう4〜5年経ち、それに伴ってMQTT Brokerも使われてきていますが、Black Hat 2017で[刑務所のドア開放や電車の不正操作も？ 無防備なMQTTのM2M接続](http://ascii.jp/elem/000/001/538/1538505/)と言われてみたり、警察庁から[IoTやM2M等で使用されるプロトコルMQTTによる探索行為の増加等について](https://www.npa.go.jp/cyberpolice/important/2017/201710061.html)と注意喚起されるなど、まだまだ微妙な使われ方をしている気がします。

各クラウドには **IoT** を冠するサービスが提供されており、要件にハマるならばクラウドサービスのMQTT Brokerを使うのが一番だと思いますが、様々な事情によって自力でMQTT Brokerを構築しなければならないこともあるでしょう。

そこで今回は、[Kubernetes](https://kubernetes.io/) ([Azure AKS](https://azure.microsoft.com/ja-jp/services/container-service/)) 上に[VerneMQ](https://vernemq.com/)の[公式 Docker image](https://github.com/erlio/docker-vernemq)を用いてMQTT over TLSクラスタを構成してみます。

# 検証した環境

|Kubernetesクラスタ|バージョン|
|:--|:--|
|AKS|Microsoft AKS 米国中部|
|Kubernetes|1.8.10|

|検証用端末|バージョン|
|:--|:--|
|azure cli|2.0.31|
|kubectl|1.10.0|
|OS|macOS Sierra 10.12.6|

# AKSの起動
Azure CLIを使って、さくっとAKSを起動します。

注意） AKSを起動するためには、Service Principalを作成できる権限が必要です。もし403エラーが発生するようでしたら、Azureアカウントの管理者に相談してください。

## Azure CLIのログイン

```bash:login_cli
$ az login
```

oauth2で認証するためのキーコードが表示されますので、 https://microsoft.com/devicelogin にブラウザでアクセスしてキーコードを入力し、Azure CLIを認証してください。

## リソースグループを作成

```bash:create_resourcegroup
$ az group create --name aks --location centralus
```

残念ながらAKSはまだプレビューのため、特定のリージョンでしか使えません。今回は米国中央リージョンにAKS用のリソースグループを作ります。

## AKS起動

```bash:start_aks
$ az aks create --resource-group aks --name aksCluster --node-count 1 --ssh-key-value $HOME/.ssh/azure.pub
```

米国中央にAKSを起動します。現時点でAKSは1.7系か1.8系のKubernetesに対応しており、 `--kubernetes-version`を指定しなかった場合は1.8系で対応している最新バージョンが選択されるようです。

また今回は検証なのでworker nodeは1つにしていますが、実際は負荷量を考えて2以上のnode数に調節してください。

## kubectlを設定

```bash:configure_kubectl
$ az aks get-credentials --resource-group aks --name aksCluster
```

起動したAKSに接続するように、kubectlを設定します。

```bash:get_nodes
$ kubectl get nodes
NAME                       STATUS    ROLES     AGE       VERSION
aks-nodepool1-37708792-0   Ready     agent     16m       v1.8.10
```

上記のように、`aks-...`というnodeがリストされていれば、AKSに接続しています。

## （オプション）Kubernetes dashboardの表示

```bash:open_dashboard
$ az aks browse --resource-group aks --name aksCluster --disable-browser
```

別のコンソールから`az aks browse`コマンドを実行すると、AKSのManagerとのトンネルを開設します。トンネルが開設されている状態で http://127.0.0.1:8001/ にアクセスすると、Kubernetes dashboardを参照することができます。

# VerneMQクラスタの構成
今回は、[VerneMQ](https://vernemq.com/)を用いてMQTT Brokerを構成します。VerneMQは比較的新しい[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)のMQTT Brokerで、Erlangで書かれており最初から水平分散できるように作られています。また公式Docker Imageが、Kubernetesクラスタも意識して作られているところが高ポイントです。

## ユーザー名とパスワードをKubernetes Secretに登録

VerneMQは、クライアントが接続する際にユーザー名とパスワードによる認証を強制することができます（認証無しにすることもできますが、 `allow_anonymous = on` を設定ファイルで明示的に指定する必要があります）。

VerneMQは、このパスワードの設定をフラットファイルやDB（PostgreSQL、MySQL、MongoDB、Redis）に持つことができますが、今回はKubernetes Secret経由で各VerneMQ Podにフラットファイルとして供給することにします。

### パスワードファイルを生成

```bash:create_passwordfile
$ mkdir -p secrets
$ touch secrets/vmq.passwd
$ docker run --rm -v $(PWD)/secrets:/mnt -it erlio/docker-vernemq vmq-passwd /mnt/vmq.passwd inner
Password:
Reenter password:
$ docker run --rm -v $(PWD)/secrets:/mnt -it erlio/docker-vernemq vmq-passwd /mnt/vmq.passwd outer
Password:
Reenter password:
```

VerneMQの公式docker imageの `vmq-passwd` コマンドを用いて、Kubernetes内部から接続する際に用いるユーザー `inner` と、インターネット越しに接続するユーザー `outer` を作り、 `secrets/vmd.passwd` ファイルへ書き込みます。

### パスワードファイルをKubernetes Secretに登録

```bash:register_password
$ kubectl create secret generic vernemq-passwd --from-file=./secrets/vmq.passwd
$ kubectl get secrets
NAME                  TYPE                                  DATA      AGE
default-token-g7pb8   kubernetes.io/service-account-token   3         1h
vernemq-passwd        Opaque                                1         13s
```

生成したパスワードファイルを、 `vernemq-passwd` としてKubernetes Secretに登録します。

## オレオレサーバー証明書をKubernetes Secretに登録

VerneMQは、生のMQTTパケットを受け付けるListerだけでなく、MQTT over TLSパケットを受け付けるListerを立てることができます（デフォルトの設定では、MQTT over TLS Listerは起動しません）。

VerisignやLet’s Encrypt等のパブリックな認証局からサーバー証明書を発行してもらって利用することもできますが、今回は自己認証局からオレオレサーバー証明書を発行して用いることにします。

### 自己認証局の証明書と秘密鍵の生成

```bash:generate_selfca
$ openssl req -new -x509 -days 365 -extensions v3_ca -keyout secrets/ca.key -out secrets/ca.crt
Generating a 1024 bit RSA private key
...++++++
....................++++++
writing new private key to 'secrets/ca.key'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:TIS Inc.
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Kubernetes-VerneMQ
Email Address []:
```

自己認証局の証明書と秘密鍵を生成します。今回は検証用ですので、Country NameやOrganization Name、Common Nameは何でもかまいません。

### 自己認証局を用いてサーバー証明書を発行

```bash:generate_servercertification
$ openssl genrsa -out secrets/server.key 2048
```
```bash:generate_servercertification
$ openssl req -out secrets/server.csr -key secrets/server.key -new
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:TIS Inc.
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:<FQDN of MQTTS endpoint>
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
```
```bash:generate_servercertification
$ openssl x509 -req -in secrets/server.csr -CA secrets/ca.crt -CAkey secrets/ca.key -CAcreateserial -out secrets/server.crt -days 365
```

自己認証局によって署名されたサーバー証明書を発行します。Common Nameには検証用として利用できるFQDNを設定してください。

### 証明書類をKubernetes Secretに登録

```bash:register_certification
$ kubectl create secret generic vernemq-certifications --from-file=./secrets/ca.crt --from-file=./secrets/server.crt --from-file=./secrets/server.key
$ kubectl get secrets
NAME                     TYPE                                  DATA      AGE
default-token-g7pb8      kubernetes.io/service-account-token   3         1h
vernemq-certifications   Opaque                                3         54s
vernemq-passwd           Opaque                                1         43m
```

自己認証局のルート証明書、設定したFQDNに対するサーバー証明書とその秘密鍵をKubernetes Secretに登録します。

## VerneMQクラスタの起動

```bash:start_vernemq_cluster
$ kubectl apply -f vernemq-cluster.yaml
```

VerneMQの公式Docker Image（[erlio/docker-vernemq](https://hub.docker.com/r/erlio/docker-vernemq/)）の現時点での最新版 1.3.1 を用いてKubernetes上にVerneMQクラスタを起動します。ポイントは、次の6つです。

* StatefulSetを用い、VerneMQのPODを一つずつ順番に起動させる
* Erlangが内部的に用いるポートを9100から9109に限定し、containerPortとして開放する
* Kubernetes SecretをPODにvolumeとしてマウントし、パスワードファイルや証明書ファイルのパスをVerneMQに設定する
* 環境変数に `DOCKER_VERNEMQ_DISCOVERY_KUBERNETES` を "1" として指定し、 `MY_POD_NAME` にPOD名を参照させることで、POD起動時に自動的にクラスタを構成させる
* VerneMQがクラスタを構成する際にkube-dnsによる名前解決が必要となるため、クラスタを構成する際に問い合わされるポート（4369）に対するHeadless Serviceを構成し、StatefulSetの`serviceName`はそのHeadless Serviceを参照する
* 暗号化されないMQTT（1883/tcp）はClusterIP Serviceとして設定することで、Kubernetes内部からのみアクセス可能にし、一方TLSで暗号化されるMQTTS（8883/tcp）はLoadBalancer Serviceとして設定することで、Internetからの接続を可能にする

```yaml:vernemq-cluster.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: vernemq
spec:
  serviceName: vernemq
  replicas: 3
  selector:
    matchLabels:
      app: vernemq
  template:
    metadata:
      labels:
        app: vernemq
    spec:
      containers:
      - name: vernemq
        image: erlio/docker-vernemq:1.3.1
        ports:
        - containerPort: 1883
          name: mqtt
        - containerPort: 8883
          name: mqtts
        - containerPort: 4369
          name: epmd
        - containerPort: 44053
          name: vmq
        - containerPort: 9100
        - containerPort: 9101
        - containerPort: 9102
        - containerPort: 9103
        - containerPort: 9104
        - containerPort: 9105
        - containerPort: 9106
        - containerPort: 9107
        - containerPort: 9108
        - containerPort: 9109
        env:
        - name: MY_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: DOCKER_VERNEMQ_DISCOVERY_KUBERNETES
          value: "1"
        - name: DOCKER_VERNEMQ_ERLANG__DISTRIBUTION__PORT_RANGE__MINIMUM
          value: "9100"
        - name: DOCKER_VERNEMQ_ERLANG__DISTRIBUTION__PORT_RANGE__MAXIMUM
          value: "9109"
        - name: DOCKER_VERNEMQ_LISTENER__VMQ__CLUSTERING
          value: "0.0.0.0:44053"
        - name: DOCKER_VERNEMQ_LISTENER__SSL__DEFAULT
          value: "0.0.0.0:8883"
        - name: DOCKER_VERNEMQ_LISTENER__SSL__CAFILE
          value: "/etc/ssl/ca.crt"
        - name: DOCKER_VERNEMQ_LISTENER__SSL__CERTFILE
          value: "/etc/ssl/server.crt"
        - name: DOCKER_VERNEMQ_LISTENER__SSL__KEYFILE
          value: "/etc/ssl/server.key"
        - name: DOCKER_VERNEMQ_VMQ_PASSWD__PASSWORD_FILE
          value: "/etc/vernemq-passwd/vmq.passwd"
        volumeMounts:
        - mountPath: /etc/ssl
          name: vernemq-certifications
          readOnly: true
        - mountPath: /etc/vernemq-passwd
          name: vernemq-passwd
          readOnly: true
      volumes:
      - name: vernemq-certifications
        secret:
          secretName: vernemq-certifications
      - name: vernemq-passwd
        secret:
          secretName: vernemq-passwd
---
apiVersion: v1
kind: Service
metadata:
  name: vernemq
  labels:
    app: vernemq
spec:
  clusterIP: None
  selector:
    app: vernemq
  ports:
  - port: 4369
    name: empd
---
apiVersion: v1
kind: Service
metadata:
  name: mqtt
  labels:
    app: mqtt
spec:
  type: ClusterIP
  selector:
    app: vernemq
  ports:
  - port: 1883
    name: mqtt
---
apiVersion: v1
kind: Service
metadata:
  name: mqtts
  labels:
    app: mqtts
spec:
  type: LoadBalancer
  selector:
    app: vernemq
  ports:
  - port: 8883
    name: mqtts
```

> 動作確認したYAML定義は、https://github.com/nmatsui/kubernetes-vernemq にあります

VerneMQの公式Dockerイメージは、本来ならVerneMQの設定ファイル（`/etc/vernemq/vernemq.conf`)に記載する設定項目を、環境変数経由で与えることができます。
設定パラメータを環境変数で与える場合、 `DOCKER_VERNEMQ_`を頭に付け、パラメータ名を大文字にしてその後に続けます（もしパラメータ名に `.` が含まれる場合、 `__`（アンダースコア２つ）に置換します）。 上記の設定YAMLも、その機能を利用しています。

## VerneMQクラスタの構成状態の確認

```bash:check_cluster
$ kubectl exec vernemq-0 -- vmq-admin cluster show
+---------------------------------------------------+-------+
|                       Node                        |Running|
+---------------------------------------------------+-------+
|VerneMQ@vernemq-0.vernemq.default.svc.cluster.local| true  |
|VerneMQ@vernemq-1.vernemq.default.svc.cluster.local| true  |
|VerneMQ@vernemq-2.vernemq.default.svc.cluster.local| true  |
+---------------------------------------------------+-------+
```

正しく設定されていれば、上記のように3台のPODで一つのVerneMQクラスタが自動的に構成されます。

## MQTTSエンドポイントの外部IPアドレス確認と名前解決

```bash:check_externalip
$ kubectl get services -l app=mqtts
NAME      TYPE           CLUSTER-IP   EXTERNAL-IP     PORT(S)          AGE
mqtts     LoadBalancer   10.0.23.42   WW.XXX.YY.ZZZ   8883:31783/TCP   13m
```

LoadBalancer Serviceとして構成したMQTTSの外部IPを確認して、サーバー証明書を作成した際に指定したFQDNで名前解決できるようにします。
（nameserverを持っていない場合は、検証用端末の `/etc/hosts` に設定すれば良いでしょう）

# 接続確認
## Kubernetes内部でsubscriberを起動

```bash:start_inner_suscriber
$ kubectl run inner-sub --rm -it --image efrecon/mqtt-client /bin/ash
mosquitto_sub -h mqtt -p 1883 -t /foo/bar -d -u inner -P <password of 'inner'>
```

Kubernetes内にmosquitto_subコマンドを持ったPODを起動し、ユーザー名とパスワードを指定して1883/tcpポートに接続します（通信経路の暗号化は行わないため、ルート証明書のパスを与える必要はありません）。

## 検証用端末でsubscriberを起動

```bash:start_outer_suscriber
$ mosquitto_sub -h <FQDN of MQTTS endpoint> -p 8883 --cafile ./secrets/ca.crt -t /foo/bar -d -u outer -P <password of 'outer'>
```

検証用端末からInternet経由で8883/tcpポートに接続します。ユーザー名とパスワードに加えて、通信経路をTLSで暗号化するために自己認証局のルート証明書のパスも引数に与えます。

## Kubernetes内部からメッセージをpublish

```bash:publish_message_from_inner
$ kubectl run inner-pub --rm -it --image efrecon/mqtt-client /bin/ash
mosquitto_pub -h mqtt -p 1883 -t /foo/bar -d -u inner -P <password of 'inner'> -m "Message from inner"
```

Kubernetes内部から1883/tcpポートに対してメッセージをpublishすると、Kubernetes内部のsubscriberにも、検証端末のsubscriberにもメッセージがpushされてきます。

```text:inner_subscriber
...
Client mosqsub|5-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from inner
...
```
```text:outer_subscriber
...
Client mosqsub|24012-Nobuyukin received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from inner
...
```

## 検証用端末からメッセージをpublish

```bash:publish_message_from_outer
$ mosquitto_pub -h <FQDN of MQTTS endpoint> -p 8883 --cafile ./secrets/ca.crt -t /foo/bar -d -u outer -P <password of 'outer'> -m "Message from outer"
```

検証用端末からInternet経由で8883/tcpポートにメッセージをpublishすると、Kubernetes内部からpublishした場合と同様、Kubernetes内部のsubscriberにも、検証端末のsubscriberにもメッセージがpushされてきます。

```text:inner_subscriber
...
Client mosqsub|5-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from outer
...
```
```text:outer_subscriber
...
Client mosqsub|24012-Nobuyukin received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from outer
...
```

# さいごに
ということで、パスワード認証 & TLS暗号化を行うMQTT Brokerクラスタを、Kubernetes上にさくっと起動することができました。今回は試していませんが、VerneMQはクライアント証明書による認証や、ユーザーごとに利用できるTOPICの制限をすることも可能です。上手く活用して、幸せなKubernetes & MQTTライフを過ごしましょう！