Ansible 角色工程化实战:Galaxy 规范、变量分层与 Molecule 测试
团队用 Ansible 久了都会走到这一步:playbook 越写越长,同样的装包、改配置逻辑在十几个文件里复制粘贴,于是拆角色。但拆完只是开始——角色没有规范的话,很快又会变成一批互相覆盖变量、改一处坏三处的大杂烩。本文以 ansible-core 2.16 以上版本为例(Molecule 6.x,版本号口径以实际安装为准),按 Galaxy 规范把一个角色该有的样子立起来:目录与元信息、变量分层、handlers 与模板的写法,再用 Molecule 把「能跑且幂等」做成每次提交前的自动验证。示例角色名为 web_base,仓库地址用 example.com 占位。
前置条件
- 控制节点已安装 ansible-core、ansible-lint,本机 Docker 可用(作为 Molecule 的默认测试驱动);
- 角色的目标平台已明确,例如 Rocky Linux 9 与 Ubuntu 22.04 两系,不要试图一个角色兼容所有发行版;
- 代码托管在 Git 仓库里,团队已约定用语义化版本打 tag;
- 手边有一个待工程化的现成角色,或准备按本文从零新建一个。
第一步:Galaxy 规范的目录与元信息
用官方脚手架生成骨架,不要自己发明目录名——Galaxy 规范的目录是 Ansible 加载角色时的约定,名字错了就不会被识别。meta/main.yml 是最容易被糊弄的文件:galaxy_info 里的平台、最低版本要如实填写,dependencies 里把依赖的角色和版本写死,不要留空让使用者去猜。tests 目录放示例 playbook,既是测试入口也是最直观的用法文档。
ansible-galaxy role init web_base
web_base/
├── defaults/main.yml # 对外可调的默认值
├── files/ # 静态文件
├── handlers/main.yml
├── meta/main.yml # 元信息与依赖声明
├── molecule/ # 测试场景(第二步之后生成)
├── tasks/main.yml
├── templates/ # Jinja2 模板
├── tests/ # 示例 playbook
└── vars/main.yml # 角色内部变量
---
galaxy_info:
author: your-name
description: Base configuration for web servers
license: MIT
min_ansible_version: "2.16"
platforms:
- name: EL
versions: ["9"]
- name: Ubuntu
versions: ["22.04"]
dependencies:
- src: example.common_base
version: 1.2.0
第二步:变量分层,先把边界划清
变量打架是角色用久了之后最常见的故障源,分层就一条原则:defaults/main.yml 放「希望使用者能改」的量,它在 Ansible 变量优先级里最低,会被清单变量、playbook 变量顺手覆盖,正适合放端口、开关、路径这类配置项;vars/main.yml 放「角色内部常量」,优先级高、外部很难覆盖,只放软件包名、服务名这类不该被乱改的东西。把需要外部调整的量错放进 vars,是新手最常踩的坑,症状就是「我在 playbook 里改了变量却不生效」。发行版差异单独成文件,在 tasks 开头按 os_family 加载,避免在每个任务里写 when 判断。
# defaults/main.yml —— 对外可调,优先级低
web_base_port: 8080
web_base_enable_firewall: true
# vars/main.yml —— 角色内部常量,不要放需要被外部覆盖的量
web_base_service_name: webapp
web_base_config_dir: /etc/webapp
# tasks/main.yml 开头:按发行版加载对应变量文件
- name: 按发行版加载变量
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "vars/{{ ansible_facts['os_family'] }}.yml"
- "vars/default.yml"
第三步:handlers 与模板的规范
handlers 只做「配置变了才触发」的动作,如重载服务;任务里用 notify 精确通知,同类动作可以用 listen 聚合成一个名字,避免多个任务各写一套重启逻辑。模板有三条纪律:第一行加 {{ ansible_managed }} 注释,让人一眼看出文件是托管生成的、不要手改;落盘前用 validate 做语法校验,校验不过就不覆盖原文件,这是防「推一份坏配置全网生效」的最后一道闸;dest 的权限在任务里写死,不要依赖 umask。下面的例子给 sshd 配置片段加了 sshd -t 校验,校验失败时任务直接报错、原配置不受影响。
# handlers/main.yml
- name: Reload sshd
ansible.builtin.service:
name: sshd
state: reloaded
listen: reload sshd
# tasks 中的模板任务:落盘前先校验
- name: 下发 sshd 配置片段
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config.d/10-web-base.conf
mode: "0644"
validate: "/usr/sbin/sshd -t -f %s"
notify: reload sshd
第四步:用 Molecule 把测试做成习惯
角色改完能不能用、连跑两遍会不会重复变更,以前靠人肉上测试机,现在交给 Molecule。它用 Docker 起干净容器,依次执行创建、汇聚(converge,实际跑角色)、幂等校验(再跑一遍,断言没有 changed)、验证、销毁,全流程一条命令。场景配置写在 molecule/default/molecule.yml:平台镜像用自建的、带 init 系统的测试镜像(地址占位为 registry.example.com),需要测 systemd 行为时给容器开 privileged 并挂载 cgroup;converge.yml 就是引用角色的示例 playbook。幂等校验是 Molecule 最有价值的一环——大量「第二次跑就报错或重复重启」的角色问题都死在这一关。
---
driver:
name: docker
platforms:
- name: rocky9
image: registry.example.com/ansible-test/rocky9:latest
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
command: /usr/sbin/init
provisioner:
name: ansible
verifier:
name: ansible
molecule test # 全流程:create → converge → idempotence → verify → destroy
molecule converge # 只重跑角色,调试时用
molecule login -h rocky9 # 进测试容器里排查
第五步:版本、发布与团队协作纪律
角色当成软件来发版:每次发布打语义化版本的 git tag,破坏兼容的变量改名必须升大版本并在 README 写清迁移方法;下游 playbook 用 requirements.yml 把来源和版本锁死,不允许引用浮动的主干分支,否则今天能跑的剧本下周就可能被上游改坏。README 是角色的门面,至少包含变量表(名称、默认值、用途)、支持的平台、一份可直接跑的示例 playbook。协作纪律再加两条:提交前本地过 ansible-lint 与 molecule test,并把这两步写进 CI;示例 playbook 同时充当 converge 测试入口,文档和测试共用一份,不会出现「文档写的和实际跑的不一样」。
# 下游 requirements.yml:版本锁定,不用浮动分支
roles:
- src: https://example.com/git/web_base.git
scm: git
version: v1.4.0
name: web_base
ansible-galaxy install -r requirements.yml -p ./roles
ansible-lint roles/web_base
验证
- 目录结构与 Galaxy 规范一致,meta/main.yml 无待改的占位项,依赖版本已锁定;
- molecule test 全流程通过,其中 idempotence 阶段无 changed 任务;
- 拿示例 playbook 在一台测试机连跑两遍,第二遍无变更、无 handler 被触发;
- ansible-lint 无 error 级告警,README 的变量表与 defaults/main.yml 逐项对得上。
常见问题
在 playbook 里改了变量却不生效
十有八九是把该量写进了 vars/main.yml,它的优先级高于清单变量和 playbook 变量,自然盖不掉。先按分层原则搬家到 defaults;排查时用 debug 打印变量的实际生效值,或用 ansible-config / 清单工具核对来源,不要靠猜。顺带记住优先级的大方向:defaults 最低,vars 偏高,命令行 -e 的 extra vars 最高。
Molecule 的 idempotence 阶段过不去
报告会指出哪个任务在第二次执行时仍然 changed。常见元凶是 command / shell 类任务和少数非声明式写法:给 command 加 creates / removes,或用 changed_when 明确变更判定;配置类文件回归 template、copy 等声明式模块,让「状态一致即无动作」成为默认。逐个任务修,不要用关掉幂等检查的方式蒙混过关。
测试全绿,生产还是出问题
Docker 测试环境和真实机器有天然差距:容器里的 init 系统、内核参数、云厂商 agent 都和生产不同。涉及 systemd、sysctl、真实网络配置的角色,要么用带完整 systemd 的测试镜像,要么用 Molecule 的 delegated 驱动把场景打到虚拟机;关键角色在发版前保留一台真实测试机做回归,绿的不只是 Molecule 报告。
小结
角色工程化的核心是把「约定」写进结构里:Galaxy 目录保证可识别,变量分层保证可覆盖,validate 与 handler 规范保证推送安全,Molecule 保证每次改动都过一遍幂等验证,再加上版本锁定和 README 纪律,角色才能从「能用的脚本」变成「团队敢依赖的资产」。







