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 纪律,角色才能从「能用的脚本」变成「团队敢依赖的资产」。

发表回复

后才能评论