はじめに
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のログイン
$ az login
oauth2で認証するためのキーコードが表示されますので、 https://microsoft.com/devicelogin にブラウザでアクセスしてキーコードを入力し、Azure CLIを認証してください。
リソースグループを作成
$ az group create --name aks --location centralus
残念ながらAKSはまだプレビューのため、特定のリージョンでしか使えません。今回は米国中央リージョンにAKS用のリソースグループを作ります。
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を設定
$ az aks get-credentials --resource-group aks --name aksCluster
起動したAKSに接続するように、kubectlを設定します。
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
aks-nodepool1-37708792-0 Ready agent 16m v1.8.10
上記のように、aks-...
というnodeがリストされていれば、AKSに接続しています。
(オプション)Kubernetes 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にフラットファイルとして供給することにします。
パスワードファイルを生成
$ 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に登録
$ 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等のパブリックな認証局からサーバー証明書を発行してもらって利用することもできますが、今回は自己認証局からオレオレサーバー証明書を発行して用いることにします。
自己認証局の証明書と秘密鍵の生成
$ 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は何でもかまいません。
自己認証局を用いてサーバー証明書を発行
$ openssl genrsa -out secrets/server.key 2048
$ 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 []:
$ 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に登録
$ 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クラスタの起動
$ 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からの接続を可能にする
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クラスタの構成状態の確認
$ 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アドレス確認と名前解決
$ 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を起動
$ 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を起動
$ 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
$ 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されてきます。
...
Client mosqsub|5-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from inner
...
...
Client mosqsub|24012-Nobuyukin received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from inner
...
検証用端末からメッセージをpublish
$ 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されてきます。
...
Client mosqsub|5-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (18 bytes))
Message from outer
...
...
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ライフを過ごしましょう!