Azure
mqtt
kubernetes
vernemq
AKS

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

はじめに

IoTという言葉がバズってからもう4〜5年経ち、それに伴ってMQTT Brokerも使われてきていますが、Black Hat 2017で刑務所のドア開放や電車の不正操作も? 無防備なMQTTのM2M接続と言われてみたり、警察庁からIoTやM2M等で使用されるプロトコルMQTTによる探索行為の増加等についてと注意喚起されるなど、まだまだ微妙な使われ方をしている気がします。

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

そこで今回は、Kubernetes (Azure AKS) 上にVerneMQ公式 Docker imageを用いて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のログイン

login_cli
$ az login

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

リソースグループを作成

create_resourcegroup
$ az group create --name aks --location centralus

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

AKS起動

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を設定

configure_kubectl
$ az aks get-credentials --resource-group aks --name aksCluster

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

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の表示

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を用いてMQTT Brokerを構成します。VerneMQは比較的新しいApache 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にフラットファイルとして供給することにします。

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

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に登録

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等のパブリックな認証局からサーバー証明書を発行してもらって利用することもできますが、今回は自己認証局からオレオレサーバー証明書を発行して用いることにします。

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

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は何でもかまいません。

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

generate_servercertification
$ openssl genrsa -out secrets/server.key 2048
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 []:
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に登録

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クラスタの起動

start_vernemq_cluster
$ kubectl apply -f vernemq-cluster.yaml

VerneMQの公式Docker Image(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からの接続を可能にする
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_を頭に付け、パラメータ名を大文字にしてその後に続けます(もしパラメータ名に . が含まれる場合、 __(アンダースコア2つ)に置換します)。 上記の設定YAMLも、その機能を利用しています。

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

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アドレス確認と名前解決

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を起動

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を起動

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

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されてきます。

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

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

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されてきます。

inner_subscriber
...
Client mosqsub|5-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from outer
...
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ライフを過ごしましょう!