资讯详情

OpenStack Heat编排入门:HOT模板实战与排错指南

📅 2026/10/15 1:30:26 | 华诺云谱 👁 阅读
OpenStack Heat编排入门:HOT模板实战与排错指南
一个被问了无数遍的问题OpenStack 里想一次性拉起一套带网络、虚拟机、安全组的环境手动敲命令得敲到什么时候如果用脚本硬拼网络资源创建失败怎么办、虚机 IP 怎么回传、后面扩容怎么改我给你的答案是 Heat而 Heat 的核心载体就是 HOT 模板。这是 OpenStack 编排服务 Heat 的资源描述格式全称 Heat Orchestration Template用 YAML 书写用来声明“我要什么样的云资源组合”。这篇文章我按实际项目里写编排模板的经验把 HOT 的语法结构、设计思路、实操命令和排错心得完整过一遍适合刚接触 Heat 的初学者也适合已经写过一些模板但想系统整理的人。1. 先搞清楚 HOT 在 Heat 里扮演什么角色1.1 为什么需要 HOT 模板Heat 是 OpenStack 里的编排服务它要做的事情就是把一组相关的云资源当成一个整体去创建、更新和删除。你单独创建一台云主机很简单但一个典型业务环境往往包含网络、子网、路由、安全组、若干台云主机、浮动 IP、存储卷它们之间还有依赖关系比如云主机必须等网络创建完成才能绑定网卡。如果靠手工操作不仅要记住每一步的依赖顺序还得自己处理失败后的回滚。HOT 模板就是把这件事变成声明式配置你在文件里描述最终想要的资源状态Heat 引擎负责解析依赖、调用各自的 OpenStack 服务、最终把整套环境创建出来。用“做饭”来类比可能更好理解。手动创建资源就像你在厨房里一边看菜谱一边手忙脚乱地切菜、炒菜、调味哪个环节出错都得自己救火HOT 模板相当于你把整个菜谱提前写成一份标准流程厨师Heat照着流程执行配料资源参数齐了就开火哪一步火候不对还能在出错时把做坏的菜倒掉重来。1.2 HOT 和 Heat 的老格式 CFN 模板有什么差别Heat 早期兼容的是 AWS CloudFormation 风格的模板叫 CFN 格式。后来 OpenStack 社区觉得这种格式偏向 AWS 的写法很多 OpenStack 特有的资源类型表达起来不够直观于是推出了 HOT 格式后缀通常是.yaml或.hot。HOT 在语法上更简洁比如参数定义用的是parameters而不是Parameters资源用resources而不是Resources整体结构更贴合 OpenStack 的术语习惯。现在日常写模板基本都以 HOT 为主CFN 格式主要用于兼容老项目或者迁移场景新项目不建议再从 CFN 起步了。HOT 的版本声明也很有辨识度。模板顶部固定要写heat_template_version: 2015-10-15或更新的版本号。这个版本号决定了哪些语法特性可用比如conditions条件判断是较新版本才支持的。在 Train 之后的版本里你还可以看到heat_template_version: yoga这样的写法照样能识别。1.3 HOT 能覆盖哪些典型场景环境初始化比如拉一套带负载均衡、多台 Web 节点、数据库节点的业务环境定制化镜像配置搭配cloud-init或者user_data在虚机第一次启动时完成软件安装和配置环境克隆把测试环境模板稍作参数修改变成生产环境或验收环境自动扩缩容结合 Heat 的OS::Heat::AutoScalingGroup和OS::Heat::ScalingPolicy在一定告警条件下增加或减少实例数跨层资源编排网络层、计算层、存储层在同一个模板里统一管理。如果你只是创建一个单台虚机用 Horizon 控制台手动点几下就行杀鸡不必用牛刀。但只要是两个以上资源之间有依赖、需要一套环境反复创建多次、或者希望环境能在必要时一键销毁重建HOT 就非常值得用。2. HOT 模板的骨架和关键语法2.1 最小可用的模板是什么样先看一个最简例子不创建任何资源只作为语法起点heat_template_version: 2015-10-15 description: A minimal HOT template. resources: {}这段内容本身没有实际业务价值但它说明了 HOT 的必要组成一个版本声明一个可选的描述一个resources字段。真实模板里参数、输出、条件都是在这个骨架上扩展。resources的内容是模板的核心它是一个映射表每个 key 是你在模板里用的资源逻辑名。注意逻辑名不等于 OpenStack 里创建出来的实际资源名。实际资源名由properties里的name字段指定如果你不指定Heat 会自动根据栈名和逻辑名拼接生成一个名字。resources: my_instance: type: OS::Nova::Server properties: flavor: m1.small image: cirros这段表示创建一台 Nova 云主机flavor 为m1.small镜像为cirros。这就是一个可直接执行的资源声明。2.2 参数定义把容易变的配置暴露出来一个面向真实使用的模板不应该把 flavor、镜像 ID、网络 ID 这种经常变化的参数硬编码在资源里面。应该把它们声明为参数然后在资源里引用。parameters: image_id: type: string description: Image ID or name to use for instances flavor: type: string default: m1.small network_id: type: string description: Network ID for the instance port参数支持的 type 主要有string、number、json、comma_delimited_list、boolean。常用约束有constraints比如设置取值范围、正则校验、长度限制parameters: instance_count: type: number constraints: - range: { min: 1, max: 10 } description: Instance count must be between 1 and 10引用参数的方式是{ get_param: image_id }引用后的结果是一个在部署时才会解析的值。这让模板具备很强的复用性——同一套模板传入不同的参数就能部署出不同规格的环境。2.3 资源定义和依赖关系的表达资源定义是模板里分量最重的部分。每个资源有逻辑名、类型、属性三块。类型决定了 Heat 调用哪个 OpenStack 服务的哪个 API比如OS::Nova::Server创建云主机OS::Neutron::Net、OS::Neutron::Subnet、OS::Neutron::Router网络相关OS::Neutron::Port、OS::Neutron::SecurityGroup端口和安全组OS::Cinder::Volume、OS::Cinder::VolumeAttachment云硬盘和挂载OS::Heat::Stack嵌套栈OS::Heat::AutoScalingGroup弹性伸缩组依赖关系既有显式的也有隐式的。显式依赖用depends_on声明resources: instance: type: OS::Nova::Server depends_on: [ port ] properties: networks: - port: { get_resource: port }隐式依赖是你在属性里通过get_resource引用了另一个资源的输出Heat 能根据这个引用自动推断出顺序。实际经验是我更推荐用属性引用表达依赖能少写很多depends_on。比如云主机要挂到某个端口上你在networks属性里引用了端口的逻辑名Heat 自然知道先建端口再建云主机。get_resource是 HOT 里非常重要的函数。它返回的是被引用资源的一个属性具体属性名要看资源类型比如OS::Neutron::Net可以返回show属性里的一系列字段OS::Nova::Server可以直接返回name、networks等。最常见的用法是把资源本身传给下游资源properties: network: { get_resource: app_net }get_attr则是获取资源创建完成后暴露出来的属性常用于输出。比如outputs: instance_ip: description: Floating IP address of the instance value: { get_attr: [ floating_ip, floating_ip_address ] }2.4 输出栈创建完成后回传关键信息输出段的作用是把创建完成的资源的关键信息展示给用户或者传给上层调用方。比如虚机创建好之后你往往需要知道它的私有 IP 和浮动 IP在命令行输出里就能直接看到。定义方式outputs: private_ip: description: Private IP address of the instance value: { get_attr: [ instance, first_address ] }嵌套栈场景中父栈可以通过get_attr拿到子栈的outputs再传给其他资源或展示出来。这种做法在做多环境标准化交付的时候特别有用比如子栈负责创建数据库环境父栈只关心子栈输出的连接地址整体模块化。2.5 条件判断按环境决定要不要建某个资源HOT 从2015-10-15版本开始支持条件语法适合做“生产环境建浮动 IP、测试环境不建”这类需求。条件定义在conditions下然后在资源属性里用condition引用。conditions: create_floating_ip: { equals: [ { get_param: env_type }, production ] } resources: floating_ip: type: OS::Neutron::FloatingIP condition: create_floating_ip properties: floating_network: { get_param: external_net }条件里的逻辑可以使用and、or、not、equals组合功能不复杂但能显著提高模板的适应性。3. 从零编写一套 VPC 多虚机的 HOT 模板3.1 先理清需求再动手我每次写模板都会先在纸上画一下资源之间的关系。比如现在要部署一套典型的 Web 环境一个自定义网络、一个子网、一个路由器连到外部网络一个安全组放行 SSH 和 HTTP两台云主机各挂一块数据卷再绑定一个浮动 IP。画完关系图之后资源清单和依赖顺序基本就出来了创建网络app_net在app_net下创建子网app_subnet创建路由器app_router设置外部网关并添加内部接口创建安全组web_sg写入允许 SSH 和 HTTP 的规则创建两台虚机web_instance_1、web_instance_2挂到子网套用安全组创建数据卷并挂载到虚机给其中一台虚机绑定浮动 IP。按照这个清单去写模板就不容易出现“不知道下一个资源该写什么”的问题。3.2 完整可用的模板示例下面这份模板是简化并验证过的思路适合直接拿去做实验。需要注意镜像、flavor、外部网络名这些需要按实际环境调整。heat_template_version: 2015-10-15 description: Deploy two web instances with network, router and security group. parameters: image_id: type: string description: Image ID or name used for instances flavor: type: string default: m1.small description: Flavor used for instances external_network: type: string description: External network name or ID for router gateway and floating IP key_name: type: string description: SSH key pair name default: mykey volume_size: type: number default: 20 description: Size of cinder volume in GB resources: app_net: type: OS::Neutron::Net properties: name: app-net app_subnet: type: OS::Neutron::Subnet properties: name: app-subnet network: { get_resource: app_net } cidr: 192.168.100.0/24 gateway_ip: 192.168.100.1 app_router: type: OS::Neutron::Router properties: name: app-router external_gateway_info: network: { get_param: external_network } interfaces: - subnet: { get_resource: app_subnet } web_sg: type: OS::Neutron::SecurityGroup properties: name: web-security-group rules: - protocol: tcp port_range_min: 22 port_range_max: 22 remote_ip_prefix: 0.0.0.0/0 - protocol: tcp port_range_min: 80 port_range_max: 80 remote_ip_prefix: 0.0.0.0/0 web_instance_1: type: OS::Nova::Server properties: name: web-instance-1 image: { get_param: image_id } flavor: { get_param: flavor } key_name: { get_param: key_name } networks: - port: { get_resource: web_port_1 } security_groups: - { get_resource: web_sg } user_data_format: RAW user_data: | #!/bin/bash yum install -y nginx systemctl enable nginx systemctl start nginx web_instance_2: type: OS::Nova::Server properties: name: web-instance-2 image: { get_param: image_id } flavor: { get_param: flavor } key_name: { get_param: key_name } networks: - port: { get_resource: web_port_2 } security_groups: - { get_resource: web_sg } web_port_1: type: OS::Neutron::Port properties: name: web-port-1 network: { get_resource: app_net } fixed_ips: - subnet_id: { get_resource: app_subnet } web_port_2: type: OS::Neutron::Port properties: name: web-port-2 network: { get_resource: app_net } fixed_ips: - subnet_id: { get_resource: app_subnet } volume_1: type: OS::Cinder::Volume properties: size: { get_param: volume_size } name: web-data-1 volume_att_1: type: OS::Cinder::VolumeAttachment properties: instance_uuid: { get_resource: web_instance_1 } volume_id: { get_resource: volume_1 } mountpoint: /dev/vdb floating_ip: type: OS::Neutron::FloatingIP properties: floating_network: { get_param: external_network } port_id: { get_resource: web_port_1 } outputs: web_instance_1_ip: description: Fixed IP of web instance 1 value: { get_attr: [ web_port_1, fixed_ips, 0, ip_address ] } web_instance_2_ip: description: Fixed IP of web instance 2 value: { get_attr: [ web_port_2, fixed_ips, 0, ip_address ] } floating_ip_address: description: Floating IP address value: { get_attr: [ floating_ip, floating_ip_address ] }这份模板里我特意先把端口单独定义出来而不是让 Nova 直接去自动创建端口。原因很简单端口单独定义后我可以在web_port_1上绑浮动 IP也可以后续在其他资源里精确引用这个 IP。如果你直接让 Nova 自动分配网络确实省事但后续再想拿这个 IP 做安全组策略或绑定浮动 IP就得通过get_attr间接取语法绕一些出错概率也高一点。3.3 写模板时的几个设计细节网络三件套分开定义不要图省事使用OS::Neutron::Net的auto_subnet之类的简化写法。生产环境中网络、子网、路由经常需要独立调整分开定义方便在 update 时只修改其中一部分而且依赖关系更直观。安全组规则建议放在安全组资源内部定义。HOT 里也支持独立的OS::Neutron::SecurityGroupRule资源但规则一多每个规则一个资源会让模板篇幅膨胀可读性下降。如果规则很多我会选择把所有规则都放在rules列表里除非有特殊需求要按条件创建某条规则。user_data 里注意格式。Nova 的user_data属性默认需要确认格式是RAW还是EC2。很多镜像期望的是纯 shell 脚本如果你不写user_data_format: RAWHeat 可能会用默认格式去解析导致你的初始化脚本不执行。我第一次踩这个坑的时候排查了很久最后发现就是少了一行声明。参数默认值不等于真实资源名。模板中尽量用参数代替硬编码的镜像 ID、flavor 名、网络 ID。因为不同项目、不同区域这些 ID 几乎都不一样硬编码之后换环境就必须改模板。参数化之后部署栈时通过-P传参一个模板通吃多个项目。4. 用命令行和 Heat 把模板跑起来4.1 环境准备与语法校验环境变量是最先要检查的。在命令行操作 Heat 之前先确认openstack命令能正常连上云环境执行下面命令查看可用端点openstack endpoint list --service orchestration如果能看到 public、internal 的 endpoint说明编排服务可用。然后可以对模板做语法和结构校验。Heat 提供openstack orchestration template validate命令在正式创建栈之前先跑一遍openstack orchestration template validate --template app_stack.yaml如果返回了完整的模板内容没有报错说明 YAML 语法和参数定义基本没问题。如果缺少必填参数它会直接告诉你哪个参数有问题。养成先 validate 再 create 的习惯能省掉很多排错时间。4.2 创建栈创建栈的命令很简单openstack stack create --template app_stack.yaml --parameter image_idcentos-7 --parameter external_networkpublic --parameter key_namemykey web-env-stack这里web-env-stack是栈名。执行后可以用openstack stack show web-env-stack查看状态常见的有CREATE_IN_PROGRESS、CREATE_COMPLETE、CREATE_FAILED。如果失败用openstack stack event list web-env-stack --format json把事件列表里resource_name、resource_status_reason拿出来看基本能定位到是哪个资源失败、失败原因是什么。这是排查 Heat 问题最主要的手段。4.3 更新栈改模板比删了重来更安全业务环境创建完后难免要加资源、改安全组规则、换镜像重新部署。直接在原栈上更新是个好选择。Heat 会根据新旧模板的差异决定哪些资源要新建、哪些要更新、哪些要删除。openstack stack update --template app_stack_v2.yaml --parameter image_idcentos-8 web-env-stack有几个更新时要注意的点虚机镜像切换时Heat 默认不会重建虚机它只会更新能通过 Nova API 直接改的属性。如果你明确要替换虚机需要在模板里改逻辑名或使用overwrite相关参数让旧资源被替换。更新安全组规则时可能可以原地生效也可能导致虚机安全组重新关联留意业务是否受影响。更新网络 CIDR 这类关键参数可能导致依赖它的子网和路由接口重建底层会有一段资源中断时间生产环境要选在维护窗口操作。4.4 删除栈一键清理全套环境删除栈时会按依赖顺序删除资源。命令openstack stack delete --yes --wait web-env-stack如果你在模板里设置了enable_remove之类的属性Heat 会遵从。但是有几个坑云主机如果设置了delete_termination为 false或者有数据盘不想被删资源可能会残留。删除前想清楚哪些数据需要保留。如果资源被外部手动改成不合法状态导致 Heat 无法正常调用删除 API栈会一直卡在DELETE_IN_PROGRESS。这时候检查事件详情看看是哪个资源卡住。必要时只能手动先删除外部资源或者使用openstack stack abandon把栈标记为孤儿并停止跟踪。4.5 查看输出信息栈创建完成后输出段的信息需要单独查看openstack stack output list web-env-stack openstack stack output show web-env-stack web_instance_1_ip如果模板里输出引用的属性在资源创建时不可用输出会显示为空或者查询报错。这时候可以等状态稳定后再查一次。5. 常见问题与排查技巧实录5.1 资源创建一直卡在 IN_PROGRESS这种现象多半是依赖链上的某个资源在等待一个永远无法满足的条件。最常见的是 Neutron 路由器接口。OS::Neutron::Router的interfaces属性在 HOT 里虽然好用但它需要子网接口能够成功添加如果外部网关所选的外部网络没有正确配置或者子网 CIDR 和已有路由冲突接口会一直卡住。我处理这个问题的思路是先用openstack stack event list看是哪个资源卡住再用openstack router show router_name手工确认路由器状态最后在openstack subnet show里看接口是否真的加上去了。如果发现是外部网络配置问题直接修正模板中的external_gateway_info参数再重新 update 栈。5.2 参数校验提示镜像或 flavor 不存在很多时候不是模板问题而是 Heat 所在项目没有权限看到你传的镜像或 flavor。还有一种是镜像名在不同项目间重名Heat 解析时取到了错误镜像。我的建议是参数优先传 UUID不要只传名称。尤其是在多项目共用 OpenStack 环境的场景下同名镜像在不同项目里指向不同实体光靠名称根本不可控。在模板的 description 里也顺手写清楚这个镜像用于什么场景后续维护的人不会一头雾水。5.3 get_attr 取不到值最常见的原因是在资源还没完全 ready 时就尝试读取属性。Heat 的依赖推导基于资源间引用关系但如果你在outputs里直接引用了一个早期资源的属性而那个属性要等某个后期动作完成后才填充就会出现取不到或取到空值的情况。比如浮动 IP 的floating_ip_address属性在绑定端口前可能为空。解决办法是先确认资源状态如果还是取不到把浮动 IP 的绑定动作放到显式依赖后面或者直接在OS::Neutron::FloatingIP创建时就把port_id绑定好这样属性在创建完成后一定可用。5.4 更新栈时资源被意外替换Heat 判断资源是否需要替换有一套规则。逻辑名不变、类型不变、大多数属性变了它会尝试原地 update但如果逻辑名变了、类型变了或者某些不可变属性变了则必须删除重建。我遇到过最坑的情况某个资源逻辑名没变只是改了name属性但它实际对应的是 Neutron 网络而 Neutron 不允许重命名网络于是 Heat 选择了删除重建导致整个依赖链上的资源全部重建。这种问题很难完全避免只能在做大版本变更前认真看openstack stack update --dry-run的结果确认哪些资源会被替换再决定要不要执行。5.5 嵌套栈的父栈创建失败子栈残留如果用了OS::Heat::Stack嵌套子栈父栈失败回滚时默认应该会级联删除子栈。但我遇到过子栈被手动修改过或者子栈依赖的外部资源被锁定导致删除不干净。现在我的习惯是嵌套子栈尽量做成无状态资源父栈回滚时能快速清理不在子栈里保留需要人工确认的数据。5.6 热模板常见坑速查表问题现象可能原因建议处理方案虚机 user_data 没执行缺user_data_format: RAW在 server 属性中显式声明参数传了但没生效资源属性里没用get_param引用检查引用写法某个资源事件报 403项目权限不足或资源类型不可用确认角色和 quota 配置输出为空属性引用时间过早或资源未 ready调整依赖绑定完成后再取属性资源一直删不掉外部手动改过资源属主调查后用栈 abandon 处理6. 版本差异与社区现状Heat 是 OpenStack 里存在已久的老项目但很多人误以为它没什么发展了。实际上 Heat 的模板版本一直在演进新版 OpenStack 里常见的版本号有rocky、train、yoga、2023.1等。较新的版本在条件语法、资源类型覆盖、嵌套栈的错误传递上都有改进。在写模板的时候先确定你所在环境的 Heat 版本能支持到什么程度。比如conditions从2015-10-15版本开始支持repeat循环语句在更晚的版本才可用。如果团队里有多套环境版本不统一最好把模板的版本声明统一写到较低能兼容的版本否则可能出现低版本环境无法解析高版本模板的情况。还有一点值得关注社区里容器化、Kubernetes 编排火热很多人问 Heat 会不会被替代。从我实际使用看Heat 的价值在于和 Neutron、Nova、Cinder 这些 OpenStack 原生服务的深度集成尤其是网络和安全组编排这是很多外部编排工具做不到的。在传统虚机为主的企业私有云里Heat 仍然是自动化部署环境的第一选择而容器场景更适合直接把 K8s 作为编排层Heat 在这类场景下更多承担“创建 K8s 节点所依赖的基础环境”这个角色。7. 生产环境使用 Heat 的个人建议写模板永远比读模板容易但读模板的能力才是把项目长期维护下去的关键。团队里如果多人维护同一套模板我强烈建议在最开头写好模块注释说清楚这套模板部署什么环境、哪些参数必填、哪些资源是核心资源、哪些扩展资源允许删除。YAML 不长的时候注释显得多余但模板写到几百行之后注释能救你的命。另一个建议是把公共片段抽成嵌套模板或自定义资源。如果你的多个环境栈都要创建同一套基础网络三件套把那部分抽成子栈父栈统一引用。这样网络策略调整时只改子栈模板不必在每一套环境里分别改。这个重构做完之后维护成本下降非常明显。我踩过最深的坑是生产环境里为了图快把模板里所有的name属性都省略指望 Heat 自动生成资源名。结果有一次排查网络问题Neutron 里看到的资源名和模板逻辑名完全对不上管理员从 Web 控制台根本看不出来哪个资源属于哪个栈只能靠标签和经验去匹配。后来我再写模板必填name属性的资源一律明确命名就算名字由参数拼接也要保证可读性。最后说一个细节使用openstack stack update前一定要先跑一次不带实际更新的 dry-run 检查。Heat 提供的--dry-run参数能帮你预览这次更新会新增、更新、替换哪些资源而不会真的动环境。我见过太多人 skip 了这一步把端口名一改结果整个网络链重建了。花一分钟看检查结果比事后恢复环境节省的时间多得多了。8. 最后分享一个小技巧把 HOT 模板纳入版本管理绝对是个被低估的好习惯。我自己的项目目录里每个环境栈都有独立的目录模板按日期迭代命名Git 仓库里保留每一次改动记录。当你需要回滚一次变更时直接找到上一个版本的模板重新 update 栈比你在脑子里回忆“当初模板长啥样”靠谱一万倍。还有一个小技巧是善用openstack stack output show获取部署后的 IP 和地址信息再结合自动化脚本做环境信息汇总。写一个简单的 Bash 脚本先create stack再循环output show最后把 IP、浮动 IP、端点信息统一输出到一个环境清单文件这套流程用来交付验收环境特别顺手。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑