
1. 项目概述为什么选择在Azure上自动化部署EMQX如果你正在物联网领域折腾或者负责一个需要处理海量设备连接的后台系统那么“MQTT Broker”这个词对你来说一定不陌生。简单来说它就像物联网世界的“电话总机”负责协调成千上万的设备客户端进行消息的发布和订阅。而EMQX无疑是这个领域里的明星选手以其高性能、高并发和丰富的企业级功能著称。但问题来了怎么把它快速、稳定、可重复地部署到云上手动在Azure门户里点点点那太容易出错了而且下次换个环境或者需要扩容时又得重来一遍。这正是我选择“Terraform Azure”这个组合拳的原因。Terraform作为基础设施即代码IaC的标杆工具能让你用声明式的配置文件描述出整个云环境该有的样子——从虚拟机、网络到安全组。一旦写好一键部署环境完全一致并且版本可控。这个项目的核心价值就是将一个复杂的生产级EMQX集群部署过程固化成一个可版本管理、可一键执行的自动化脚本。无论你是个人开发者想快速搭建测试环境还是团队需要一套标准的部署流程这套方案都能让你从繁琐的手工操作中解放出来把精力集中在业务逻辑上。接下来我会带你从零开始拆解每一个环节不仅告诉你怎么做更会解释为什么这么做以及我踩过的那些坑。2. 核心架构与资源规划在动手写代码之前清晰的架构设计是成功的一半。我们的目标是在Azure上部署一个高可用的EMQX集群。这里的高可用不仅仅指EMQX服务本身还包括其依赖的网络、存储等基础设施的可靠性。2.1 网络拓扑设计一个安全的云服务绝不能把数据库或消息中间件直接暴露在公网上。我们的设计遵循最小权限和网络分层原则。首先我们需要一个虚拟网络VNet。在这个VNet内部我们会创建两个子网应用子网用于放置EMQX Broker的虚拟机。这个子网将承载主要的MQTT流量。管理子网用于放置Bastion Host堡垒机或未来可能的管理工具。所有对EMQX虚拟机的SSH或管理端口的访问都必须先通过堡垒机跳转杜绝从公网直接访问后端服务器的风险。其次是网络安全组NSG规则。这是云安全的关键防线。对于EMQX虚拟机我们需要精细地控制入口流量MQTT端口1883通常仅对内部VNet或特定的客户端IP范围开放。如果设备在公网强烈建议使用MQTTS8883并配合证书。MQTTS端口8883用于加密通信。WebSocket端口8083和WSS端口8084供浏览器等客户端使用。Dashboard端口18083EMQX的管理界面端口。务必将其访问权限限制在管理子网或你的办公网络IP绝不能对0.0.0.0/0开放。集群通信端口4369 4370 5369 6369 6369 4370等这些是Erlang分布式节点间通信的端口必须允许在EMQX虚拟机之间的内部流量。注意很多初次部署失败问题都出在NSG规则上。Azure的NSG是有状态防火墙意味着如果你允许了某个端口的入站规则其对应的出站响应流量会自动允许。但你需要确保你定义的规则优先级正确没有被更高优先级的拒绝规则拦截。2.2 计算与存储选型对于EMQX计算资源的选择主要看两点连接数和消息吞吐量。虚拟机型号对于测试或中小规模场景Standard_B2s或Standard_B2msB系列爆款机型性价比很高。它们提供基准CPU性能并能突发到更高性能适合流量不恒定的物联网场景。对于生产环境建议从Standard_D4s_v3或Standard_D8s_v3D系列通用型开始它们提供更稳定的CPU和更好的内存带宽。操作系统Ubuntu 20.04 LTS或22.04 LTS。这是EMQX官方文档最常推荐的环境社区支持最好能避免很多依赖库的兼容性问题。磁盘Azure虚拟机默认的OS磁盘通常是Premium SSD LRS对于系统盘和EMQX的安装来说已经足够。如果你需要极高的日志写入性能可以考虑为/var/log/emqx目录挂载一个独立的数据磁盘并选择P30或更高性能的SSD。但大多数情况下默认配置即可。2.3 Terraform模块化设计思路一个好的Terraform项目应该是模块化的。我们将创建以下模块modulesmodules/network定义VNet、子网、NSG及路由表。modules/compute定义虚拟机规模集VM Scale Set或单个虚拟机、网络接口、公共IP如果需要的话、磁盘。modules/emqx这个模块不直接创建资源而是通过remote-exec或custom_datacloud-init在虚拟机启动后自动执行安装和配置EMQX的脚本。这是实现“一键部署”的灵魂所在。主配置文件main.tf则像乐高说明书一样调用这些模块并传递参数如位置、资源前缀、VM尺寸等将它们组装成一个完整的系统。这种设计让代码复用性极高未来部署到不同环境开发、测试、生产时只需修改变量文件即可。3. Terraform配置详解与实操现在我们进入核心的实操环节。请确保你本地已经安装了Terraform CLI和Azure CLI并且已经通过az login登录了你的Azure账户。3.1 项目初始化与提供商配置首先创建一个项目目录并初始化Terraform工作区。mkdir azure-emqx-terraform cd azure-emqx-terraform创建providers.tf文件声明我们要使用的Azure提供商及其版本。固定版本号是个好习惯可以避免未来提供商升级导致语法不兼容。# providers.tf terraform { required_version 1.0 required_providers { azurerm { source hashicorp/azurerm version ~ 3.0 # 使用3.x版本与最新Azure API兼容性好 } } } provider azurerm { features {} # 如果没有设置环境变量可以在这里指定subscription_id # subscription_id your-subscription-id }然后创建variables.tf来定义我们需要的输入变量。这提高了脚本的灵活性。# variables.tf variable resource_group_name { description The name of the resource group where all resources will be created. type string default rg-emqx-prod } variable location { description The Azure region where resources will be deployed (e.g., eastus, westeurope). type string default East US } variable admin_username { description The admin username for the virtual machine. type string default azureuser sensitive true } variable admin_ssh_public_key { description The SSH public key for VM authentication. Its safer than passwords. type string sensitive true } variable emqx_package_url { description The download URL for the EMQX package. Using the latest stable version is recommended. type string default https://www.emqx.com/en/downloads/broker/5.0/emqx-5.0.26-ubuntu20.04-amd64.deb } variable vm_size { description The size of the virtual machine for EMQX. type string default Standard_B2ms } variable vnet_address_space { description The address space for the virtual network in CIDR notation. type list(string) default [10.0.0.0/16] } variable app_subnet_prefix { description The address prefix for the application subnet. type string default 10.0.1.0/24 }接着创建outputs.tf部署成功后我们需要知道EMQX Dashboard的访问地址和虚拟机IP。# outputs.tf output emqx_public_ip { description The public IP address of the EMQX virtual machine. value azurerm_public_ip.emqx_vm_pip.ip_address } output emqx_dashboard_url { description The URL to access EMQX Dashboard. REMEMBER: Its only accessible from allowed IPs defined in NSG. value http://${azurerm_public_ip.emqx_vm_pip.ip_address}:18083 } output bastion_host_public_ip { description The public IP address of the Bastion Host (if deployed). value try(azurerm_public_ip.bastion_pip[0].ip_address, Bastion not deployed) }3.2 核心资源定义网络与安全现在我们来创建网络模块。首先建立modules/network/main.tf。# modules/network/main.tf resource azurerm_resource_group rg { name var.resource_group_name location var.location } resource azurerm_virtual_network vnet { name ${var.prefix}-vnet address_space var.vnet_address_space location azurerm_resource_group.rg.location resource_group_name azurerm_resource_group.rg.name } resource azurerm_subnet app_subnet { name app-subnet resource_group_name azurerm_resource_group.rg.name virtual_network_name azurerm_virtual_network.vnet.name address_prefixes [var.app_subnet_prefix] } resource azurerm_network_security_group app_nsg { name ${var.prefix}-app-nsg location azurerm_resource_group.rg.location resource_group_name azurerm_resource_group.rg.name security_rule { name AllowSSHFromBastion priority 100 direction Inbound access Allow protocol Tcp source_port_range * destination_port_range 22 source_address_prefix var.bastion_subnet_prefix # 只允许从堡垒机子网访问SSH destination_address_prefix * } security_rule { name AllowMQTT priority 110 direction Inbound access Allow protocol Tcp source_port_range * destination_port_range 1883 source_address_prefix var.allowed_mqtt_clients_cidr # 限制客户端IP范围 destination_address_prefix * } security_rule { name AllowMQTTS priority 120 direction Inbound access Allow protocol Tcp source_port_range * destination_port_range 8883 source_address_prefix var.allowed_mqtt_clients_cidr destination_address_prefix * } security_rule { name AllowDashboardFromOffice priority 130 direction Inbound access Allow protocol Tcp source_port_range * destination_port_range 18083 source_address_prefix var.office_ip_cidr # 仅允许办公室IP访问管理界面 destination_address_prefix * } # 注意Erlang集群端口规则需要在虚拟机之间开放通常通过Azure VNet的默认规则或额外的NSG规则实现此处为简化未列出全部。 } resource azurerm_subnet_network_security_group_association app_nsg_assoc { subnet_id azurerm_subnet.app_subnet.id network_security_group_id azurerm_network_security_group.app_nsg.id }对应的modules/network/variables.tf和outputs.tf也需要定义好输入输出变量。3.3 虚拟机部署与EMQX自动化安装这是最精彩的部分让虚拟机在启动时自动安装配置EMQX。我们将使用Azure的custom_data功能它支持cloud-init脚本适用于Linux。在modules/compute/main.tf中定义虚拟机资源# modules/compute/main.tf data azurerm_subnet app { name app-subnet virtual_network_name azurerm_virtual_network.vnet.name resource_group_name azurerm_resource_group.rg.name depends_on [module.network] # 显式依赖网络模块 } resource azurerm_network_interface emqx_nic { name ${var.prefix}-emqx-nic location azurerm_resource_group.rg.location resource_group_name azurerm_resource_group.rg.name ip_configuration { name internal subnet_id data.azurerm_subnet.app.id private_ip_address_allocation Dynamic public_ip_address_id azurerm_public_ip.emqx_pip.id } } resource azurerm_public_ip emqx_pip { name ${var.prefix}-emqx-pip location azurerm_resource_group.rg.location resource_group_name azurerm_resource_group.rg.name allocation_method Static # 静态IP便于DNS绑定 sku Standard } resource azurerm_linux_virtual_machine emqx_vm { name ${var.prefix}-emqx-vm location azurerm_resource_group.rg.location resource_group_name azurerm_resource_group.rg.name size var.vm_size admin_username var.admin_username network_interface_ids [ azurerm_network_interface.emqx_nic.id, ] admin_ssh_key { username var.admin_username public_key var.admin_ssh_public_key } os_disk { caching ReadWrite storage_account_type Premium_LRS } source_image_reference { publisher Canonical offer 0001-com-ubuntu-server-focal sku 20_04-lts-gen2 # 使用Ubuntu 20.04 LTS Gen2镜像 version latest } # 核心通过custom_data传递cloud-init脚本 custom_data base64encode(templatefile(${path.module}/cloud-init.yaml, { emqx_package_url var.emqx_package_url admin_username var.admin_username })) # 依赖项确保网络接口配置完成 depends_on [ azurerm_network_interface.emqx_nic, azurerm_public_ip.emqx_pip, ] }关键的cloud-init.yaml脚本内容如下。这个脚本会在虚拟机首次启动时执行。# modules/compute/cloud-init.yaml #cloud-config package_update: true package_upgrade: true packages: - wget - curl - vim runcmd: # 1. 下载并安装EMQX - wget ${emqx_package_url} -O /tmp/emqx.deb - sudo dpkg -i /tmp/emqx.deb || sudo apt-get install -f -y # 自动修复依赖 - rm /tmp/emqx.deb # 2. 可选修改默认配置文件例如允许外部IP访问Dashboard生产环境慎用 # 这里仅为示例生产环境应通过更安全的方式管理配置。 - sudo sed -i s/^listeners.http.default.bind 127.0.0.1:18083/listeners.http.default.bind 0.0.0.0:18083/ /etc/emqx/emqx.conf # 3. 启动EMQX服务并设置开机自启 - sudo systemctl start emqx - sudo systemctl enable emqx # 4. 检查服务状态 - sleep 10 # 等待服务完全启动 - sudo systemctl status emqx --no-pager # 5. 输出初始登录信息密码在首次启动时生成 - echo EMQX installation and initial configuration completed. - echo The default dashboard credentials are: - echo Username: admin - echo Password: (Check the file /etc/emqx/emqx_auth_username.conf or run sudo emqx_ctl admins passwd admin new_password to set)实操心得cloud-init的runcmd模块是按顺序执行的。如果某一步失败后面的命令可能不会执行。务必在关键步骤如dpkg -i后添加错误处理|| true或|| apt-get install -f -y。另外systemctl start之后最好加一个sleep给服务足够的启动时间再检查状态。3.4 整合与部署最后在根目录的main.tf中调用我们定义的模块。# main.tf module network { source ./modules/network resource_group_name var.resource_group_name location var.location prefix emqxprod vnet_address_space var.vnet_address_space app_subnet_prefix var.app_subnet_prefix bastion_subnet_prefix 10.0.2.0/24 # 示例堡垒机子网 allowed_mqtt_clients_cidr 10.0.0.0/16 # 示例允许整个VNet内的设备 office_ip_cidr 203.0.113.0/24 # 替换为你的公网IP段 } module compute { source ./modules/compute resource_group_name var.resource_group_name location var.location prefix emqxprod vnet_name module.network.vnet_name app_subnet_name app-subnet admin_username var.admin_username admin_ssh_public_key var.admin_ssh_public_key emqx_package_url var.emqx_package_url vm_size var.vm_size depends_on [module.network] # 明确声明依赖关系 }创建一个terraform.tfvars文件来赋值变量注意此文件通常被.gitignore忽略以保护敏感信息。# terraform.tfvars resource_group_name my-emqx-rg location East US admin_username azureadmin admin_ssh_public_key ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...你的公钥内容现在执行部署三部曲# 1. 初始化下载Azure提供商插件 terraform init # 2. 预览执行计划检查将要创建、变更或销毁的资源 terraform plan # 3. 确认无误后执行部署 terraform apply -auto-approve # 使用-auto-approve跳过确认生产环境建议手动确认部署成功后控制台会输出emqx_dashboard_url和emqx_public_ip。记住此时你还无法直接访问Dashboard因为NSG规则只允许了办公室IP。你需要通过堡垒机如果部署了跳转或者临时将你的当前公网IP添加到office_ip_cidr变量中并重新apply。4. 部署后配置、验证与问题排查部署完成只是第一步确保EMQX按照预期运行并安全配置至关重要。4.1 初始登录与安全加固获取默认密码通过堡垒机SSH到EMQX虚拟机。ssh -J bastion_userbastion_ip azureadminemqx_private_ip登录后查看EMQX生成的默认密码文件sudo cat /etc/emqx/emqx_auth_username.conf或者直接使用命令行工具修改密码更推荐sudo emqx_ctl admins passwd admin MyNewStrongPassword123!访问Dashboard使用上面输出的URLhttp://public_ip:18083和修改后的密码登录。首次登录后立即修改密码配置监听器在Dashboard的“管理” - “监听器”页面检查MQTT、MQTTS等监听器是否已启动并绑定在正确的IP上通常是0.0.0.0。确保生产环境中的MQTT监听器1883不要对公网开放应通过负载均衡器或API网关来暴露服务。4.2 功能验证使用mosquitto_pub和mosquitto_sub这类MQTT客户端工具进行快速测试。# 在另一台可以访问EMQX私有IP的机器上 # 订阅一个主题 mosquitto_sub -h emqx_private_ip -p 1883 -t test/topic -v # 发布一条消息到同一主题 mosquitto_pub -h emqx_private_ip -p 1883 -t test/topic -m Hello from Terraform!如果订阅端能收到消息说明MQTT Broker基础功能正常。4.3 常见问题与排查实录即使自动化程度很高一些问题仍可能发生。以下是我在实际操作中遇到过的典型问题及解决方法。问题现象可能原因排查步骤与解决方案Dashboard (18083) 无法访问1. NSG规则未正确配置或优先级错误。2. EMQX服务未运行或配置未绑定到0.0.0.0。3. 操作系统防火墙UFW未开放端口。1. 在Azure门户检查虚拟机关联的NSG确认有允许你IP访问18083端口的入站规则且优先级高于可能的拒绝规则如默认的DenyAllInBound。2. SSH到虚拟机执行sudo systemctl status emqx查看服务状态。检查/etc/emqx/emqx.conf中listeners.http.default.bind的值。3. 运行sudo ufw status。如果UFW是激活状态需运行sudo ufw allow 18083。MQTT客户端连接超时或拒绝连接1. NSG未开放1883/8883端口给客户端IP段。2. 客户端网络无法路由到Azure VNet。3. EMQX的MQTT监听器未启动。1. 同上检查NSG规则。2. 如果客户端在公网确保虚拟机有公网IP且NSG允许。更佳实践是使用Azure VPN Gateway或ExpressRoute建立混合连接。3. 在Dashboard或通过sudo emqx_ctl listeners命令检查监听器状态。terraform apply失败提示资源冲突或提供商错误1. Azure资源名称重复。2. Terraform状态文件.tfstate与实际资源不一致。3. 提供商版本不兼容。1. 确保prefix或资源名全局唯一。2. 谨慎使用terraform import导入已有资源或terraform state rm移除错误状态。操作前备份.tfstate文件。3. 检查providers.tf中的版本约束尝试terraform init -upgrade。cloud-init脚本执行失败EMQX未安装1. 脚本语法错误。2. 网络问题导致wget下载失败。3. 包依赖问题。1. SSH到虚拟机查看cloud-init日志sudo cat /var/log/cloud-init-output.log。这是最直接的排错入口。2. 在脚本中添加重试机制或使用更稳定的镜像源。3. 在dpkg -i后加入 服务启动成功但负载很高或连接不稳定1. 虚拟机规格vCPU/内存不足。2. 系统参数如文件描述符限制、TCP参数未优化。1. 监控Azure门户中的虚拟机指标CPU、内存、网络。根据负载考虑升级VM型号。2. 针对EMQX进行系统调优。编辑/etc/sysctl.conf和/etc/security/limits.conf增加文件描述符限制和TCP连接参数然后重启。EMQX官方文档有详细的性能调优指南。踩坑提醒最隐蔽的问题往往是网络和安全组。Azure的NSG规则是应用到子网或网络接口的并且有优先级。一个常见的错误是你添加了允许规则但Azure默认的DenyAllInBound规则优先级是65500最低理论上不会拦截。但如果存在其他自定义的拒绝规则且优先级更高你的允许规则就会失效。务必在门户中仔细检查规则列表和优先级顺序。5. 生产环境进阶考量与优化一套可用于生产的部署方案远不止“跑起来”那么简单。以下是一些进阶的优化方向你可以根据项目需求逐步引入。5.1 高可用与集群化单点故障是生产环境的大忌。EMQX支持集群化我们可以利用Azure可用性集Availability Sets或虚拟机规模集VM Scale Sets来实现。使用VM Scale Sets修改modules/compute将azurerm_linux_virtual_machine资源替换为azurerm_linux_virtual_machine_scale_set。这样可以轻松指定实例数量如2-3个并实现自动修复和简单负载均衡。配置EMQX集群EMQX节点通过一个共享的数据库如Mnesia或通过ETCD/K8s来发现彼此。在cloud-init脚本中我们需要动态获取当前节点的私有IP并配置emqx.conf中的node.name和集群发现方式。例如使用静态节点列表方式# 在cloud-init脚本中假设我们知道其他节点的IP echo cluster.discovery static /etc/emqx/emqx.conf echo cluster.static.seeds emqx10.0.1.4, emqx10.0.1.5 /etc/emqx/emqx.conf更优雅的方式是使用Azure Tags、Metadata Service或者外部Consul/ETCD服务进行自动发现。5.2 安全加固升级使用Azure Key Vault管理密钥将EMQX的SSL证书、Dashboard密码等敏感信息存储在Azure Key Vault中。Terraform可以读取Key Vault中的Secret在部署时通过custom_data注入避免硬编码在脚本或配置文件中。启用TLS/SSL加密生产环境必须使用MQTTS8883和WSS8084。你可以通过Terraform的tls提供商生成自签名证书仅测试或使用azurerm_key_vault_certificate资源引用从CA购买的证书。部署Azure Application Gateway作为WAF在EMQX前端部署应用网关可以提供SSL终止、基于路径的路由、以及Web应用防火墙WAF功能防护常见的Web攻击为Dashboard和WebSocket端口增加一道防线。5.3 监控与日志“可观测性”是运维的基石。Azure Monitor为虚拟机启用诊断设置将系统日志和性能指标发送到Log Analytics工作区。你可以设置警报规则例如当CPU持续高于80%时触发告警。EMQX监控集成EMQX自身提供了丰富的Prometheus格式指标。你可以在虚拟机上安装Prometheus Node Exporter来收集系统指标并配置EMQX暴露指标端点。然后使用Grafana可以部署在Azure Container Instances或另一台VM上来创建丰富的监控看板。日志聚合将EMQX的日志/var/log/emqx/*通过rsyslog或fluentd转发到Azure Log Analytics或Elasticsearch服务便于集中查询和分析。5.4 成本优化云上资源用多少付多少成本控制很重要。使用Spot虚拟机对于非核心的测试或开发环境可以使用Azure Spot虚拟机成本可能降低至标准价格的90%。只需在azurerm_linux_virtual_machine资源中设置priority Spot并配置eviction_policy。但需注意Spot实例可能被Azure回收。资源调度如果测试环境不需要7x24小时运行可以使用Azure Automation或简单的CRON作业配合Azure CLI在非工作时间执行az vm deallocate关闭虚拟机工作时间再az vm start可以节省大量计算费用。定期审查资源使用terraform plan和terraform state list定期审查删除不再使用的资源。利用Azure Cost Management工具分析支出。通过以上步骤你不仅获得了一个一键部署的EMQX环境更得到了一套可维护、可扩展、符合生产要求的基础设施代码。下次当你需要一个新的EMQX环境或者灾难恢复时只需简单地运行terraform apply一杯咖啡的时间一个与之前完全一致的环境就会准备就绪。这种确定性和效率正是Infrastructure as Code的魅力所在。