用 docker 部署 mosquitto 並編譯 mosquitto-auth-plug

mosquitto 的簡介

mosquitto 是一個開源的輕量級消息代理服務, 支持 MQTT-3.1 和 MQTT-3.1.1, 採用發佈訂閱模式. mosquitto 目前廣泛用於手機設備, 底端傳感器, 嵌入式計算機的消息通信, 是一個成熟的物聯網通信服務方案. 作爲一個用 C 編寫的應用服務, mosquiitto 項目同樣提供了 C library 便於 MQTT 服務的拓展, 比如有名的 mosquitto-auth-plug.

mosquitto 的部署

這裏我們打算部署在 docker 容器內.

在 docker 內構建

參考 eclipse-mosquitto docker hub
構建目錄 /mosquitto
目錄結構是:

- mosquitto
    - config
        - mosquitto.conf
    - data
    - log

其中 mosquitto.conf 的內容是:

persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

在 docker 內啓動

啓動命令:

$ docker run -d -p 1883:1883 -p 9001:9001 -v mosquitto/:/mosquitto/ eclipse-mosquitto

編譯 mosquitto-auth-plug

參考: https://www.loraserver.io/gui...

準備工作

注意: 在 docker 內部構建編譯環境的時候(尤其是在 alpine 環境中)會經常出現:

temporary error (try again later)

這是因爲在獲取像openssl,build-base之類包的時候, 需要從一些官方的鏡像庫中獲取, 而由於國內的"某些"網絡原因, DNS 往往不能直接解析到官方鏡像庫. 參考這篇文章:徹底解決docker build時安裝軟件失敗問題, 可以用tcpdump觀察這一現象. 所以我們最好先安排一下宿主機的 DNS.

查看本機 nameserver

根據阿里雲ECS啓動Docker容器無法訪問外網這篇文章, 我們可以這樣來獲取本機 nameserver:

$ cat /etc/resolv.conf
nameserver 10.143.xx.xxx
nameserver 10.143.xx.xxy
options timeout:2 attempts:3 rotate single-request-reopen

這裏我們可以看到有可能有多個域名解析服務地址, 這是一種容災措施, 如果其中一個解析失敗了, 在若干次嘗試後會使用另外一個服務器去解析. 我們選取其中一個就好.

然後修改/etc/docker/daemon.json, 添加:

"dns": ["10.143.xx.xxx"]

重啓守護進程:

$ sudo systemctl daemon-reload
// 最好也重啓一下docker
$ sudo systemctl restart docker

當然, 如果網絡環境足夠好(比如在國外), 可以不需要配置 DNS. 即使不配置 DNS, 也有一定的機率安裝軟件都成功.

修改軟件源爲國內加速鏡像

現在發現單純改 DNS 還是經常會下載軟件失敗, 只好修改容器的軟件源了. 在 Dockerfile 中將 aliyun 的軟件源寫入 /etc/apk/repositores:

RUN echo "http://mirrors.aliyun.com/alpine/v3.4/main/" > /etc/apk/repositories

創建宿主機文件目錄結構

目錄結構如下:

- mosquitto
    - Dockerfile
    - config
        - mosquitto.conf
        - config.mk
    - data
    - log
    - src
- postgres
    - Dockerfile
- docker-compose.yml

其中config放置 mosquitto 的啓動配置mosquitto.conf和 mosquitto-auth-plug 的編譯配置config.mk. data, log分別作爲 mosquitto 的數據存儲目錄和日誌存儲目錄, src用於放置編譯需要的源文件.

配置 mosquitto.conf

mosquitto.conf 應該聲明 mosquitto-auth-plug 插件編譯之後生產的 auth-plug.so的位置, 並說明 postgres 數據庫的連接用戶, 連接端口, 連接密碼, 以及處理權限和讀寫的 sql 語句. 參考:
Setting up ACL in Mosquitto using Postgres, 和MQTT authentication & authorization, 和mosquitto-auth-plug 在 github 上的 README.下面是一個可供參考和使用的 mosquitto.conf:

persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

allow_anonymous false

auth_plugin /mosquitto/src/mosquitto-auth-plug/auth-plug.so
auth_opt_backends postgres

auth_opt_host pgsql_db
auth_opt_port 5432
auth_opt_dbname mosquitto
auth_opt_user root
auth_opt_pass root_password

auth_opt_userquery SELECT password FROM account WHERE username = $1 limit 1
auth_opt_superquery SELECT COALESCE(COUNT(*), 0) FROM account WHERE username = $1 AND super = 1
auth_opt_aclquery SELECT topic FROM acls WHERE (username = $1) AND rw >= $2

配置 config.mk

這裏我們打算將 postgres 作爲驗證後端, 因此 config.mk 應該是這樣的:

BACKEND_CDB ?= no
BACKEND_MYSQL ?= no
BACKEND_SQLITE ?= no
BACKEND_REDIS ?= no
BACKEND_POSTGRES ?= yes 
BACKEND_LDAP ?= no
BACKEND_HTTP ?= no
BACKEND_JWT ?= no
BACKEND_MONGO ?= no
BACKEND_FILES ?= no
BACKEND_MEMCACHED ?= no

MOSQUITTO_SRC = /mosquitto/src/mosquitto/src
OPENSSLDIR = /usr/lib
SUPPORT_DJANGO_HASHERS ?= no
CFG_LDFLAGS =
CFG_CFLAGS =

OPENSSLDIR 指定爲 /usr/lib,
MOSQUITTO_SRC 指定爲/mosquitto/src/mosquitto/src, 也可以不需要指定, 因爲在容器中已經安裝了 mosquitto, 編輯腳本可以從環境中獲取源碼地址, 這樣甚至後面我們都不需要用 git 從 github 上拉取 mosquitto 的源碼了.

編寫 Dockerfile

鏡像的源來自 eclipse mosquitto 的 Docker Hub, 基於 alpine linux.
這是我根據編譯經驗寫的Dockerfile:

FROM eclipse-mosquitto
WORKDIR /mosquitto
ADD mosquitto/config /mosquitto/config

RUN apk update && \
    apk add git && \
    apk add mosquitto-dev && \
    apk add postgresql-dev && \
    apk add openssl && \
    apk add build-base && \
    git clone https://github.com/eclipse/mosquitto /mosquitto/src/mosquitto && \
    git clone https://github.com/jpmens/mosquitto-auth-plug /mosquitto/src/mosquitto-auth-plug && \
    mv /mosquitto/config/config.mk /mosquitto/src/mosquitto-auth-plug/config.mk && \
    cd /mosquitto/src/mosquitto-auth-plug && \
    make clean && make

EXPOSE 1881 9001

可以看到這裏我們安裝了以 postgresql 爲後端編譯 auth-plug 需要的依賴: mosquitto-dev, postgresql-dev, openssl, 和 C 語言的 GCC 編譯環境build-base. 用 git 拉取了 mosquitto 和 mosqtuitto-auth-plug 的源碼. 最後使用自定義的 config.mk 進行 make 編譯.

編寫 docker-compose.yml

打算用容器編排工具 docker-compose 運行編譯了 mosquitto-auth-plug 的 mosquitto 容器, 下面是一個可供使用和參考的 docker-compose.yml:

version: '3'
services:
  mosquitto:
    build: ./Dockerfile_mosquitto
    volumes:
      - ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf
    command: ['mosquitto', '-c', '/mosquitto/config/mosquitto.conf']
    ports:
      - '1881:1881'
      - '9001:9001'

部署 postgres 後端

前面已經把 mosquitto 部署成功, 但是由於這裏我們打算讓 mosquitto 的權限認證和讀寫認證交給 postgres(不然爲什麼我們要花那麼費勁去編譯 mosquitto-auth-plug?), 所以我們還要部署一個 mosquitto 可以訪問的 postgres 後端或者在已有的 postgres 上創建相關的表結構. 注意 postgres 後端的用戶, 密碼等應該和 mosquitto.conf 上配置的一致.

編寫 postgres 的 docker-compose.yml

直接在原有的 docker-compose.yml 上加一個 service:

services:
  ...
  postgres_db:
    image: postgres
    environment:
      POSTGRES_DB: mosquitto
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root_password

這裏我們不暴露 postgres 的 5432 端口, 因爲 docker 內部的同一個網絡可以直接通過容器名字來訪問對應的容器, 而且我們不希望容器外的其他人可以訪問數據庫. 注意這裏的POSTGERS_USER應該和 mosquitto.conf 中的auth_opt_user對應, 類似的也有POSTGRES_DB, POSTGRES_PASSWORD. 而容器名字postgres_db可以成爲auth_opt_host的值, docker 會爲這個 host 做 DNS 解析.

然後我們就可以直接運行 postgres 容器:

$ docker-compose up -d

初始化 postgres

爲了滿足 mosquitto-auth-plug 的業務需求, 需要爲 postgres 生成對應的表, 可以參考mosquitto-auth-plug/examples/mysql.sql

CREATE TABLE account(
id SERIAL,
username TEXT NOT NULL,
password TEXT,
super smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id)
);

CREATE INDEX account_username ON account (username);

CREATE TABLE acls (
id SERIAL,
username TEXT NOT NULL,
topic TEXT NOT NULL,
rw INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);

CREATE UNIQUE INDEX acls_user_topic ON acls (username, topic);

這些 SQL 語句也可以寫在 Dockerfile 裏面作爲第一次構建時就執行的.
接着就可以插入幾條測試數據了. 用戶的密碼生成需要使用 mosquitto-auth-plug 提供的密碼生成器, 它是以 PBKDF2 的形式儲存在數據庫後端的.

比如我們 進入 mosquitto 容器內:

$ docker exec -it <mosquitto_auth_container_id> /bin/sh

爲用戶生成一個明文爲123456的密碼:

$ /mosquitto/src/mosquitto-auth-plug/np
Enter password:
Re-enter same password:
PBKDF2$sha256$901$Yf1FSeMi1j7OjWdW$u1MqFR7iIZj+m6P7vKDvuMx+oDDJW4ub

之後爲 account 表內爲test_user添加一條記錄:

> INSERT INTO account(username, pasword, super) values (
    'test_user', 
    'PBKDF2$sha256$901$Yf1FSeMi1j7OjWdW$u1MqFR7iIZj+m6P7vKDvuMx+oDDJW4ub',
    0)

並設定test_user對於 topic /topic1爲可讀可寫:

> INSERT INTO acls(username, topic, rw) values (
    'test_user',
    '/topic1',
    2)

現在數據庫內的記錄是:

 id | username |                              password                               | super 
----+----------+---------------------------------------------------------------------+-------
  1 | test_user | PBKDF2$sha256$901$S55aebUoogFy6XSf$E6PbQWDFQ06KLFlqqR3x+NBjg6ixjwez |     0


 id | username |   topic    | rw 
----+----------+------------+----
  1 | test_user| /topic1    | 2

意味着用戶 test_user 可以用密碼123456向 topic topic1 發佈主題和訂閱主題.

搭建應用環境進行測試

我們這裏用 python 中同樣實現了 mqtt 協議的 paho-mqtt 進行測試, 如果生產環境是使用 python 的話, 也可以直接使用這個庫作爲連接庫.
安裝很簡單的:

$ pip install paho-mqtt

搭建一個 python-alpine 容器:

應用程序代碼就放在 python-alpine 容器裏好了. 下面是一個可供參考和使用的 docker-compose.yml:

services:
  mqtt_server:
    image: python-alpine
    command: ['tail']

docker-compose up mqtt_server -d 使得該容器掛起, 然後用docker exec -it <container_id> /bin/sh黑魔法進入容器. 編寫一個測試用的訂閱者(client.py):

import paho.mqtt.client as mqtt
import time

HOST = 'mosquitto'
PORT = 1883

def client_loop():
    client_id = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
    client = mqtt.Client(
        client_id=client_id, clean_session=True)
    client.username_pw_set('test_user', '123456')
    client.on_connect = on_connect 
    client.on_message = on_message
    client.connect(HOST, PORT, 60)
    client.loop_forever()

def on_connect(client, userdata, flags, rc):
    print('Connected with result code ' + str(rc))
    client.subscribe('/topic1')

def on_message(client, userdata, msg):
    print(msg.topic + ' ' + msg.payload.decode('utf-8'))

if __name__ == '__main__':
    client_loop()

然後編寫一個測試用的發佈者(publish.py):

import paho.mqtt.publish as publish
import time

HOST = 'mosquitto'
PORT = 1883

if __name__ == '__main__':
    client_id = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
    publish.single('topic1', 'hello mqtt', qos=2, hostname=HOST, port=PORT,
            client_id=client_id, auth={'username': 'test_user', 'password': '123456'})

當運行 client.py 之後, client 陷入監聽 mosquitto 的狀態, 等待收到發送在 topic topic1 的消息。 再運行一次 publish.py, 它往 topic1發送了一個消息, 我們可以看到 client.py 就已經收到了.

總結

我們成功地搭建了一個 mosquitto 的消息代理服務, 併爲它編譯了 mosquitto-auth-plug, 可以供後續後端消息業務的開發. 詳細代碼也可以參考已經上傳到 github 的 example.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章