Ansible:从 localhost 幂等实验到受控主机滚动编排
发布窗口里,应用配置文件已经从 workers=4 改成 workers=8,Ansible 的 recap 也出现了 changed=1,可服务仍按旧并发运行。值班同学重新执行 playbook,结果变成 changed=0,于是断言“配置已经正确”。真正的问题藏在两个被混为一谈的状态里:文件模块判定目标字节已经符合期望,不等于进程已经消费新配置;第一次执行时,后续任务失败又让被通知的 handler 没有在该主机上运行。第二次执行没有产生新的 changed,自然也不会再次通知 handler。
这类事故说明 Ansible 不是“把一组 Shell 命令发到机器上”的薄包装。它先用 inventory 建立主机与组的模型,再为每台主机合并变量和 fact,由 connection plugin 建立执行通道,调用 module 读取当前状态并尝试收敛,最后依据 module 返回的 changed、failed 和 unreachable 决定 handler、后续 task 以及主机是否继续留在活动集合中。读懂这条执行链,ok=12 changed=0 failed=0 才是证据的一部分,而不是一句万能的正确证明。
Control node 发起执行,managed node 承担变化
Ansible 安装在 control node 上。它读取项目、inventory 和凭据,渲染 task 参数并调度连接;managed node 是被管理对象,通常无需安装 Ansible,但许多 POSIX module 需要目标机上可用的 Python。网络设备、Windows 和 raw 等入口有自己的连接或运行要求,不能把“无 agent”误解成“目标端没有任何依赖”。ansible-core 安装指南还明确指出:Windows 本机不是受支持的原生 control node,Windows 开发机应使用 WSL、Linux 虚拟机或固定的 execution environment,而不是依赖偶然能启动的 Python 包。
最轻的形态是在开发机或 CI runner 上运行 ansible-playbook,先用 localhost 和 ansible_connection=local 验证模板、变量、handler 与幂等语义。进入共享环境后,control node 通过 SSH 管理一组 Linux 主机;团队规模继续扩大时,AWX 或 Red Hat Ansible Automation Platform 的 Automation Controller 再承接项目同步、inventory、credential、job template、RBAC、调度和运行记录。控制器并不会替你修复不幂等的 task、错误的变量模型或危险的提权边界,它只是把相同执行能力放进受控服务面。
在 Linux、macOS 或 WSL 中,先为项目建立独立 Python 环境。ansible-core 提供核心 CLI 与内置 collection;ansible 社区包还聚合了一组社区 collection。这里采用受支持的 ansible-core 2.21 发行线,它要求 control node 使用 Python 3.12 至 3.14,常规 POSIX managed node 的 Python 支持范围为 3.9 至 3.14;目标设备是否还需要其他库,继续由具体 module 文档决定。团队要把 core、control Python、目标 Python 与 collection 兼容性一起锁定,不能只写一个无上限的 pip install ansible。
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "ansible-core==2.21.*"
ansible --version
ansible-playbook --version
ansible-config dump --only-changedansible --version 应显示 core 版本、Python 解释器、module search path、collection location 和配置文件路径。版本存在却没有读取项目 ansible.cfg 时,后续 inventory、callback、forks 和 host key checking 可能全部落在另一套默认值上,因此验证输出里的 config file 比“命令能运行”更重要。项目可以采用下面的最小结构:
automation/
├── ansible.cfg
├── inventories/
│ ├── lab/hosts.yml
│ ├── lab/group_vars/all.yml
│ └── lab/host_vars/local-a.yml
├── playbooks/
│ └── converge.yml
├── roles/
│ └── demo_config/
│ ├── defaults/main.yml
│ ├── handlers/main.yml
│ ├── tasks/main.yml
│ └── templates/app.conf.j2
├── collections/requirements.yml
└── requirements.txt# ansible.cfg
[defaults]
inventory = inventories/lab/hosts.yml
roles_path = roles
collections_path = .ansible/collections
retry_files_enabled = false
interpreter_python = auto_silent
forks = 10
timeout = 15
[ssh_connection]
pipelining = trueinventory 决定默认主机来源;roles_path 和 collections_path 改变可执行内容的搜索路径,写错会加载不到依赖,也可能意外加载用户目录中的同名内容;forks 是 control node 最大并行 worker 数,不等于一次滚动批次大小;timeout 主要约束连接等待,不是整份 playbook 的总超时;pipelining 可减少部分 SSH 往返,但会受到提权配置和目标端 sudo 策略影响。用 ansible-config dump --only-changed 保存生效值证据,避免只审查文件而忽略环境变量、命令行和当前目录带来的覆盖。
Inventory 先回答“对谁执行”
Inventory 不只是 IP 清单。host 是变量和 fact 的归属单元,group 用于选择主机和分配共享变量,父子 group 表达集合关系;inventory plugin 还可从云 API、CMDB 或控制器同步动态主机。别名、连接地址与业务身份应分开:web-a 可以是稳定的 inventory hostname,ansible_host 才是当前连接地址。这样 IP 变化不会连带破坏 host_vars/web-a.yml、审计记录和 --limit web-a。
先用两个指向同一台本机的逻辑 host 观察逐 host 变量解析:
# inventories/lab/hosts.yml
all:
children:
app:
hosts:
local-a:
ansible_connection: local
release_lane: canary
local-b:
ansible_connection: local
release_lane: stable
vars:
app_workers: 4
app_owner: platform-demoansible-inventory -i inventories/lab/hosts.yml --graph
ansible-inventory -i inventories/lab/hosts.yml --host local-a
ansible app -i inventories/lab/hosts.yml -m ansible.builtin.ping--graph 应显示 @all -> @app -> local-a/local-b;--host local-a 应展开连接变量和组变量;ping 返回的 pong 证明 module 能在目标执行并返回 JSON,不证明 SSH、sudo、应用端口或配置已经正确。这里的 connection 是 local,所以两个逻辑 host 会写同一个文件;它适合暴露主机模型与真实资源不一致的风险,却不应伪装成两台独立机器。
切到 SSH 时,把连接身份放在 inventory 变量或凭据系统里,而不是散落在 task 中:
all:
children:
app:
hosts:
web-a:
ansible_host: 192.0.2.21
web-b:
ansible_host: 192.0.2.22
vars:
ansible_user: automation
ansible_port: 22
ansible_ssh_private_key_file: ~/.ssh/automation_ed25519
ansible_python_interpreter: /usr/bin/python3192.0.2.0/24 是文档示例网段。真正接入前,应先由可信通道取得服务器 host key,并把它写入专用 known_hosts;不能用 host_key_checking=False 或 StrictHostKeyChecking=no 把中间人风险改名为“自动化便利”。连接失败时用 ansible web-a -m ping -vvvv 区分 DNS/路由、host key、认证、Python 解释器和 module 失败,日志中的高详细度输出可能包含路径、用户名、命令参数与模块入参,排障产物也必须按敏感数据管理。
Play、task 与 module 怎样组成一次收敛
Play 把 host pattern、变量、执行策略和一组 task 绑定起来;task 是“对每个选中 host 调用什么动作”的声明;module 才负责读取当前状态、执行变更并返回结构化结果。ansible.builtin.template 不只是复制文本,它会在 control node 渲染 Jinja 模板,比较目标内容并按需写入;ansible.builtin.file 会检查文件属性;ansible.builtin.service 与服务管理器交互。用完全限定集合名(FQCN)能让代码评审明确调用来源,也减少 collection 中同名短模块的解析歧义。
下面的 localhost 实验把中间状态留在临时目录,不要求 root,也不会修改系统服务:
# playbooks/converge.yml
---
- name: Converge a local application configuration
hosts: app
gather_facts: true
serial: 1
vars:
demo_root: /tmp/ansible-tool-efficiency
tasks:
- name: Create the demo directory
ansible.builtin.file:
path: "{{ demo_root }}"
state: directory
mode: "0750"
- name: Render the application configuration
ansible.builtin.copy:
dest: "{{ demo_root }}/app.conf"
mode: "0640"
content: |
owner={{ app_owner }}
workers={{ app_workers }}
lane={{ release_lane }}
os={{ ansible_facts.system }}
notify: Record configuration activation
- name: Read back the effective file
ansible.builtin.slurp:
src: "{{ demo_root }}/app.conf"
register: effective_config
- name: Prove the desired worker count reached the file
ansible.builtin.assert:
that:
- "'workers=' ~ (app_workers | string) in (effective_config.content | b64decode)"
fail_msg: The effective file does not contain the desired worker count
handlers:
- name: Record configuration activation
ansible.builtin.copy:
dest: "{{ demo_root }}/activated-{{ inventory_hostname }}"
mode: "0640"
content: "activated={{ app_workers }}\n"先只选 local-a,再执行第二遍:
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--limit local-a --diff
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--limit local-a --diff
cat /tmp/ansible-tool-efficiency/app.conf
cat /tmp/ansible-tool-efficiency/activated-local-a第一遍通常会让目录、配置和 activation 文件出现变化;配置 task 的 changed 通知 handler,handler 在 tasks 段结束时执行。第二遍在输入与目标状态未变时,目录与配置应为 ok,handler 不再运行,recap 的 changed 应下降到 0。这里“第二遍不变”只能证明这些 module 在这个主机、输入和可见状态下没有检测到差异。它不能证明隐藏在外部 API、进程内存、数据库、负载均衡器或人工修改中的状态已经正确。
把命令换成 ansible.builtin.shell: echo ... >> app.conf,每一遍都会追加;把 changed_when: false 强行加给这个 task,只会把统计和 handler 通知压成 ok,并不会阻止文件继续变化。command 不经 shell 解释 >>,把 shell 语法塞给它不会产生预期重定向,这是另一类常见误判。错误处理文档明确说明,changed_when 定义报告何时算变化,也决定是否触发 handler。幂等来自 module 对当前状态与期望状态的比较以及变更动作本身可收敛,不来自把 changed 染成绿色。
Fact 与变量优先级决定每台主机看到什么
gather_facts: true 会在 play 开始阶段运行 facts action,得到系统、网络、Python、日期等主机事实,并放入 ansible_facts。Fact 是一次探测结果,不是永久真相;目标在探测后变化、fact cache 过期或不同执行环境探测能力不同时,task 看到的值可能不同。不要用 debug: var=ansible_facts 把整份主机信息写进共享日志,只输出决策所需的白名单字段。
变量可能来自 role defaults、inventory group/host vars、fact、play、task、include_vars、set_fact、role 参数和 extra vars。完整顺序应以官方变量优先级为准;工程上更重要的不是背二十多级顺序,而是让每类值只有一个常规写入位置:role defaults 放可覆盖默认值,group vars 放环境或集群共同值,host vars 只放主机差异,Vault 或外部 secret store 放密文,CI 的 -e 仅传发布批次等明确覆盖项。
用 group vars、host vars 和 extra vars 三处同名变量可以直接看到覆盖结果。Role defaults 只有在 role 进入 play 时才会参与解析,单独在磁盘创建 roles/demo_config/defaults/main.yml 并不会让任意 play 自动加载它:
# inventories/lab/group_vars/all.yml
app_workers: 4
# inventories/lab/host_vars/local-a.yml
app_workers: 6
# playbooks/show-precedence.yml
---
- name: Show the effective value
hosts: local-a
gather_facts: false
tasks:
- ansible.builtin.debug:
msg: "workers={{ app_workers }}"ansible-playbook -i inventories/lab/hosts.yml playbooks/show-precedence.yml
ansible-playbook -i inventories/lab/hosts.yml playbooks/show-precedence.yml \
--extra-vars app_workers=9local-a 同时命中 group 与 host 变量,第一条应输出 workers=6;extra vars 在官方变量优先级中位于最高层,第二条应输出 workers=9。若第一条仍为 4,先用 ansible-inventory --host local-a 检查 host vars 文件名、inventory source 和生效配置,而不是继续增加覆盖层。这也是危险所在:CI 若允许任意 extra vars,调用者可能绕过仓库中的受审默认值。Role vars 的优先级高于 inventory,但它也不是访问控制,role/include 参数和 extra vars 仍可覆盖;真正不可由调用者改变的不变量要用 assert、策略门禁和权限边界验证。可变参数要有 schema、枚举和范围断言;凭据不能通过 -e password=... 出现在进程列表、runner 元数据或 shell history 中。
Registered variable 记录某个 task 对当前 host 的返回,常见字段包括 changed、failed、rc、stdout 和 module 自定义数据。set_fact 把值写入当前 host 上下文;启用 cacheable 后还会产生优先级不同的缓存 fact。它们都不是跨系统事务记录。若回滚依赖“上次版本”,应从制品仓库、配置版本或外部部署记录读取,而不是假定下一次运行仍保留某个 registered value。
Handler 是延迟动作,不是事务提交
Handler 适合把多处配置变化合并成一次重载:多个 task notify 同一个 handler,默认在 play 的相应任务段结束时执行一次。通知依赖 task 报告 changed,不是依赖文件名或内容语义。Handler 在 play 级有全局命名空间,同名 handler 可能被后加载者遮蔽,因此 role 中应使用稳定、唯一的监听主题。
tasks:
- name: Render main configuration
ansible.builtin.template:
src: app.conf.j2
dest: /etc/demo-app/app.conf
mode: "0640"
notify: demo_app_config_changed
- name: Validate staged configuration
ansible.builtin.command: /usr/local/bin/demo-app --check-config /etc/demo-app/app.conf
changed_when: false
handlers:
- name: Reload demo application
ansible.builtin.service:
name: demo-app
state: reloaded
become: true
listen: demo_app_config_changed如果模板 task 已变化,而校验 task 随后失败,默认情况下该失败 host 上的 handler 可能不运行,于是磁盘文件与进程内状态分叉。--force-handlers 能让已通知 handler 在一般 task 失败后仍尝试执行,但 host 已不可达时依然没有魔法通道;而且“强制重载一份校验失败的配置”可能让事故更严重。更稳妥的设计是先在临时路径渲染并校验,校验成功后原子替换正式文件,再通知重载;服务重载后用健康检查证明进程接受了配置。meta: flush_handlers 可提前冲刷通知,但会改变中间状态,应只在后续 task 明确依赖已重载服务时使用。
一个失败实验拆开 FAILED、UNREACHABLE 与重试
把失败都归为“Ansible 没跑通”会让重试扩大破坏面。下面的 inventory 同时包含本机和一个保留地址,playbook 又为本机制造稳定的内容断言失败:
# inventories/lab/failure.yml
all:
hosts:
local-ok:
ansible_connection: local
unreachable-demo:
ansible_host: 127.0.0.1
ansible_port: 1
ansible_user: automation
ansible_ssh_timeout: 2# playbooks/failure.yml
---
- name: Classify failures instead of hiding them
hosts: all
gather_facts: false
tasks:
- name: Reach the execution target
ansible.builtin.ping:
- name: Produce a stable semantic failure on localhost
ansible.builtin.command: printf WRONG
register: probe
changed_when: false
failed_when: probe.stdout != "READY"ansible-playbook -i inventories/lab/failure.yml playbooks/failure.yml -vvunreachable-demo 连接本机一个未监听的端口,应进入 UNREACHABLE,并伴随 Connection refused;它会被移出本次运行的活动 host 集合。这里使用的是 ansible.builtin.ssh connection plugin,所以连接等待变量是 ansible_ssh_timeout,也可由 --timeout 或 [ssh_connection] timeout 设置;不要把其他 connection plugin 的超时参数照搬过来。local-ok 能执行 module,但因 failed_when 条件进入 FAILED。默认行为是在失败 host 上停止后续 task,同时让其他仍活动的 host 继续。若 control node 连 ssh 可执行文件都没有,错误发生在连接程序启动之前,当前 ansible-core 可能把它记成普通 task failure;这与 managed node 拒绝连接是两类证据。
ignore_errors 只处理 task 已运行并返回的 failed,不会吞掉未定义变量、语法错误或连接不可达;ignore_unreachable 才允许不可达主机继续留在后续任务路径上,但连接没有恢复时只会制造更多噪声。修复网络或认证后,可用 ansible.builtin.meta: clear_host_errors 让已经移出的主机重新进入活动集合;这不是重试,也不会补做前面已跳过的副作用。把任一 ignore 选项铺满 playbook,会让 recap 看似更平静,却把未知状态带到后续步骤。
旧式 .retry 文件不是可靠恢复计划。即使重新限制失败主机,也必须先判断已成功 host 是否已经产生不可逆副作用,以及失败 task 能否安全重放。可以从控制器运行记录或 recap 取得失败 host 集合,再执行:
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--limit 'web-a,web-b' --start-at-task 'Validate staged configuration'这里的 web-a,web-b 必须来自已经复核的失败主机记录,不能由操作者凭印象输入。但 --start-at-task 会跳过此前注册变量、动态 include 和准备动作,task 名重名也可能定位错误。更可靠的恢复方式是让 playbook 从任意已知状态重新收敛,或设计明确的 resume play;重试前读取目标实际状态,而不是机械复用第一次运行的心理模型。对暂时性 API 或服务探测,可使用 task 级 retries、delay 与 until,并把最终返回、尝试次数和失败原因留下;对配置语义错误重试十次只会延迟报警。
Role 与 collection 把复用变成供应链
Role 用约定目录封装 defaults、vars、tasks、handlers、templates、files、meta 和 tests,使一类主机配置拥有清晰输入与执行入口。Defaults 应可覆盖,vars 只放调用方不应随意改变的内部值;handler 名和变量名要加 role 前缀,避免多个 role 进入同一 play 后发生命名碰撞。Role 不是越大越好:把数据库、监控和业务发布塞入一个“base” role,会让变更评审和最小重跑都失去边界。
Collection 是更高一层的分发单元,可包含 role、module、plugin、playbook 和文档。collections: 关键字只改变短名称的搜索顺序,不会安装依赖;role 也不会自动继承调用 play 的 collection 搜索设置。官方 collection 指南因此建议在 task 中使用 FQCN,并在 requirements.yml 固定直接依赖版本:
# collections/requirements.yml
collections:
- name: community.general
version: "13.2.0"
source: https://galaxy.ansible.comansible-galaxy collection install \
-r collections/requirements.yml \
-p .ansible/collections
ansible-galaxy collection listcommunity.general 13.2.0 声明支持 ansible-core 2.18 及以上;换用其他版本时,要重新解析它的 requires_ansible 与项目 core 基线。固定直接依赖只是起点。团队还要保存解析后的 collection 清单、下载源、制品哈希或签名验证结果、对应 ansible-core 兼容范围和 execution environment 镜像 digest;在受控制品库中镜像依赖,而不是每次 CI 都从公网拉取最新内容。Module 和 lookup plugin 都可能在 control node 执行代码并接触凭据,第三方 collection 的风险不低于应用依赖。升级时先在隔离 inventory 运行 syntax、check、真实 converge 与第二遍收敛,再扩大到 canary,不把“版本号只升了一个补丁位”当作兼容证明。
requirements.yml 的精确版本只锁定直接 collection,ansible-galaxy 不会替项目生成类似语言包管理器的完整传递依赖 lockfile。可重复构建应把已解析 tarball 镜像到受控制品库,记录每个 collection 的版本与 SHA-256,并在发行服务器支持签名时通过 --keyring 和 --required-valid-signature-count 强制验证;直接从 Git、目录或任意 URL 安装时,Galaxy 的签名验证并不自动生效。一次干净 execution environment 构建完成后,保存 ansible-galaxy collection list 与镜像 digest,再用该 digest 执行门禁和发布,避免两个 job 在同一可写 collection 目录中安装不同依赖。
Vault 保护静态密文,但不会自动保护使用过程
Ansible Vault 加密 YAML 文件或字符串,适合让仓库保存密文而不是明文。它不提供动态租约、自动轮换或目标系统撤销;Vault 密码、解密后的变量、module 参数和目标配置仍要单独治理。团队通常为环境设置 vault ID,使调用者明确正在解哪一类密文:
ansible-vault encrypt_string \
--vault-id lab@prompt \
--name app_api_token 'replace-with-a-disposable-demo-value'
ansible-vault view \
--vault-id lab@/secure/path/lab-vault-password \
inventories/lab/group_vars/all/vault.ymlVault ID label 默认只是解密提示,不是密钥隔离边界。提供多个 --vault-id 时,Ansible 会优先尝试标签匹配的密码,失败后仍可能依次尝试其他已提供密码;不能据此证明 lab 凭据绝不会解开 production 内容。环境隔离仍需不同密钥材料、不同读取权限、不同 job template 和受控的密码来源。
CI 不应把 vault password 文件提交进仓库,也不应把密码直接拼在命令行。由 runner 的 secret store 在作业期间注入权限收紧的临时文件或受控脚本,作业退出即清理,并限制谁能同时读取密文仓库与启动作业。no_log: true 可以抑制 task 参数和返回出现在常规输出,但它不是数据流隔离:模板、目标日志、异常信息、callback plugin、fact cache、debug task 和下游系统仍可能泄漏值。敏感 task 周围要做真实的失败演练,确认日志、artifact、通知和控制器事件中都没有密文。
SSH 私钥、become 密码和应用 secret 是不同权限域。连接账户只需登录受控主机,become: true 应落在确实需要提权的 block 或 task,并通过 sudoers 限制可执行动作;不要让所有 play 默认以 root 运行。become_user: root 只声明目标身份,不提供授权,密码或无密码 sudo 策略仍由目标系统决定。把个人 SSH key 交给 CI、共享一个 root key 或关闭 host key 校验,都会让审计无法回答“谁在何处以什么权限改变了主机”。
Check、diff 和语法检查各自证明什么
--syntax-check 验证 playbook 能否被解析以及部分静态引用,不连接主机,也不证明变量在目标 host 上完整。--check 请求支持 check mode 的 module 预测变化;不支持 check mode 的 module 不执行且通常没有可供后续条件使用的真实结果,依赖前一步创建结果或 registered value 的 task 可能因此失真。Task 还能显式设置 check_mode: false,即使整次运行带着 --check 也会真实执行,所以预览门禁必须审查这种例外。--diff 让支持 diff 的 module 展示前后差异,但配置文件可能含口令、证书、内网地址和客户数据,不应默认把完整 diff 上传为 CI artifact。
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--syntax-check
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--limit local-a --check --diff
ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml \
--limit local-a --diff一条可信的变更证据链至少包含:静态解析成功;inventory 和 limit 选中的 host 与审批对象一致;check 输出被当作风险预览而非执行承诺;真实执行后的目标配置、服务状态和业务探针符合期望;第二遍在同一输入下没有意外变化;人工漂移能被下一遍发现并收敛。changed=0 只能说明 module 没有报告变化,check mode 的 changed=1 也只是预测。对于 command、外部 API、自定义 module 和时间相关模板,必须单独证明 check 支持与幂等条件。
Tags、limit、serial 与 strategy 控制的是不同维度
--limit 在 play 的 host pattern 结果上再取子集,适合 canary、故障 host 或某个环境;执行前用 --list-hosts 留下最终主机集合。Tags 选择 task,但 always、动态 include、依赖初始化与 handler 会让“只跑一个 tag”比看起来复杂。发布 playbook 不应依赖操作者记住一串隐含 tags,关键不变量应在任何入口都执行,危险维护动作则用独立 playbook 和显式审批。
serial 把 host 切成批次,并让一个批次完整跑完 play 后再进入下一批;它也把某些失败百分比的计算作用域收缩到当前 batch。forks 限制 control node worker 数,throttle 还能进一步限制某个 task 或 block 的并发。默认 linear strategy 让当前 task 在一批 host 上完成后再进入下一 task;free 允许快主机先向后推进,适合主机互不依赖的工作,却会打破“所有主机都完成步骤 A 后才做步骤 B”的隐含假设。
- name: Roll out the application safely
hosts: app
serial:
- 1
- "25%"
- "100%"
strategy: linear
max_fail_percentage: 0
tasks:
- name: Remove this host from traffic
ansible.builtin.command: /usr/local/bin/lbctl disable {{ inventory_hostname }}
delegate_to: localhost
throttle: 1
- name: Apply and verify the release role
ansible.builtin.include_role:
name: app_release
- name: Verify this host before returning traffic
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
register: rollout_health
retries: 6
delay: 5
until: rollout_health.status == 200
- name: Return this host to traffic
ansible.builtin.command: /usr/local/bin/lbctl enable {{ inventory_hostname }}
delegate_to: localhost
throttle: 1这里的数值只是演示批次。真实批次由副本数、容量余量、负载均衡摘挂延迟和 SLO 决定。max_fail_percentage 的判断条件是“超过阈值”而非“达到阈值”;设为 0 才表示当前批次出现任一失败就停止扩大。它不会撤销已经完成的副作用,失败主机也会保持摘流,因为返回流量的任务没有执行;后续必须由独立恢复入口判定回滚、修复或继续隔离,不能在 always 中无条件重新接流量。若使用 any_errors_fatal: true,Ansible 会完成当前批次正在执行的 fatal task 后终止整个 play,但同样不提供事务回滚。
delegate_to: localhost 还意味着 control node 必须拥有负载均衡器凭据,而且委派任务可能由多个 host 并行落到同一个端点,示例用 throttle: 1 只约束该 task 的并发,不构成跨 job 锁。若两个 CI job 同时对同一 inventory 发布,Ansible CLI 本身没有类似 Terraform state lock 的中央互斥,团队必须在流水线、控制器或外部租约中按环境加锁。
把项目接入 CI,而不是把 CI 变成共享 root 终端
CI 中应固定 Python、ansible-core、collection 和 execution environment,先运行 lint、syntax、inventory 与 localhost 实验,再对受控 canary 执行真实 converge。一个可审查的流水线顺序是:
stages:
- verify
- preview
- apply
verify:
script:
- python -m pip install -r requirements.txt
- ansible-galaxy collection install -r collections/requirements.yml -p .ansible/collections
- ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml --syntax-check
- ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml --limit local-a
- ansible-playbook -i inventories/lab/hosts.yml playbooks/converge.yml --limit local-a
preview_shared:
script:
- ansible-playbook -i inventories/shared/hosts.yml playbooks/site.yml --check --diff --limit canary
artifacts:
paths: [sanitized-preview.txt]
apply_shared:
resource_group: shared-ansible-inventory
script:
- ansible-playbook -i inventories/shared/hosts.yml playbooks/site.yml --limit canary
- ansible-playbook -i inventories/shared/hosts.yml playbooks/site.yml --limit app具体 CI 方言会不同,关键不变量相同:受信 runner 才能取得 SSH/Vault 凭据;预览输出先脱敏;apply 绑定受保护分支、审批与环境锁;job 保存代码 commit、inventory 版本、execution environment digest、调用人、最终 host 集合和 recap;取消作业时明确哪些 host 已完成,不能把“流水线红了”当作全局未变更。
Runner 工作目录也要隔离。前一个 job 留下的 decrypted vars、SSH control socket、fact cache 或 collection 会污染下一个 job。作业结束按顺序撤销临时凭据、关闭连接、删除解密文件与工作区,再保留经过脱敏的证据。共享静态 runner 若做不到可信清理,应改用一次性 runner 或容器化 execution environment。
AWX 与 Automation Controller 增加控制面,不改变 playbook 事实
AWX 是上游社区控制器;Automation Controller 是 Red Hat Ansible Automation Platform 中的受支持控制组件。两者都把 inventory、project、credential、execution environment 和 job template 组合成可重复启动的 job,并提供 API、RBAC、调度与运行历史。Job template 的 Check 类型仍受 module check mode 支持边界约束;“按钮显示成功”也仍需目标状态与业务探针佐证。
Project 通常从 Git 同步 playbook,inventory 可以手工维护或从来源同步,credential 在 job 启动时注入连接和 Vault 等认证材料,execution environment 用容器镜像固定 ansible-core、collection、Python 与系统依赖,job template 再绑定这些对象和允许启动时覆盖的参数。AWX job template 文档说明,启动时可提示 inventory、credential、limit、tags 和 extra vars;开放哪些 prompt 就等于开放哪些变更维度,不能把任意 extra vars 和任意 limit 同时交给低权限调用者。控制器版本与 execution environment 中的 core/collection 是两条升级线,不能用控制器界面版本替代实际 job 的 ansible-playbook --version 证据。
控制器适合需要集中授权、审计、调度、通知、凭据注入和团队委派的场景;少量开发机 localhost 实验或单仓库 CI 不必为了“有界面”先引入平台。平台化后还会新增自己的数据库、消息处理、execution node、镜像仓库、容量和升级责任。Execution environment 镜像应使用不可变 digest、扫描 SBOM 与漏洞、限制可拉取 registry,并在升级前重放代表性 job。动态 inventory 的“update on launch”需要明确缓存时长与失败策略,否则 job 可能使用旧主机集合,或者所有发布都阻塞在来源同步。
回滚不是反向重放每个 task
Ansible 没有跨 module 的自动事务。一个 play 可能已经写文件、安装包、重启服务并调用外部 API,第四步失败时前三步不会自动撤销。回滚应围绕可恢复对象设计:配置保留上一个受审版本,制品使用不可变版本号,数据库变更另走自己的兼容流程,外部系统操作记录幂等键和补偿动作。
- name: Activate a version with an explicit rescue path
hosts: app
serial: 1
tasks:
- block:
- name: Deploy the candidate configuration
ansible.builtin.copy:
src: "configs/{{ release_id }}/app.conf"
dest: /etc/demo-app/app.conf
mode: "0640"
backup: true
register: config_write
notify: Reload demo application
become: true
- ansible.builtin.meta: flush_handlers
- name: Verify application health
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
register: health
retries: 6
delay: 5
until: health.status == 200
rescue:
- name: Restore the last known-good configuration
ansible.builtin.copy:
remote_src: true
src: "{{ config_write.backup_file }}"
dest: /etc/demo-app/app.conf
mode: "0640"
when: config_write.backup_file is defined
notify: Reload demo application
become: true
- ansible.builtin.meta: flush_handlers
- name: Verify health after the restore attempt
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
register: rollback_health
retries: 6
delay: 5
until: rollback_health.status == 200
- name: Stop expansion after rollback
ansible.builtin.fail:
msg: Candidate failed health verification; the previous configuration was restored and its health probe passed这个骨架只有在 backup_file 存在且恢复后的健康探针通过时才会报告“已恢复”;若首次部署没有旧文件,或复制 task 在生成备份前失败,when 会跳过恢复,随后的健康探针应继续失败并保留原始未知状态,不能伪造回滚成功。还要验证包或 schema 是否兼容旧配置,以及负载均衡器是否重新接入。rescue 只捕获可进入 block 错误处理的失败,control node 崩溃、强制取消和主机不可达可能让恢复动作根本没有机会运行。团队因此要保留独立 rollback playbook,并能根据 job 事件确定最后一个完成的 host 与批次;生产配置恢复还应从受审制品取回明确版本,不能长期依赖目标机旁边一个未经保留策略治理的临时 backup 文件。
Localhost 实验的清理由专门 play 完成,避免读者使用危险的通配删除:
---
- name: Remove only the localhost experiment directory
hosts: localhost
connection: local
gather_facts: false
tasks:
- ansible.builtin.file:
path: /tmp/ansible-tool-efficiency
state: absentansible-playbook -i localhost, -c local playbooks/cleanup.yml
test ! -e /tmp/ansible-tool-efficiency长期治理看的是可重放、可归因和可退出
一个成熟的 Ansible 仓库应能回答:哪一个 commit、inventory 版本、execution environment digest 和变量集合改变了哪些 host;谁批准并启动;哪个 task 首次失败;已经完成到哪个批次;密文由哪个凭据系统注入;怎样重放、回滚和证明漂移收敛。答不出来时,再多 role 和漂亮 recap 也只是不可审计的远程执行。
变更门禁应围绕不变量运行:同一固定输入连续执行后,受管对象不继续单调变化;人工修改受管文件后,下一次运行能检测并恢复;错误 host key 必须阻断连接;无 sudo 权限的账号只能在需要提权的 task 失败;Vault 密文、diff、fact 和 verbose 日志不进入公开 artifact;canary 失败阻止后续批次;取消或 control node 故障后,能从事件与目标状态重建已完成集合。演示中的批次、重试和超时值不能直接当作生产阈值,真实值由容量预算、连接基线、变更窗口和 SLO 推导。
还要定期清理 host、group、role、collection、凭据和 job template。下线主机从动态来源消失不代表 SSH key、sudoers、known_hosts、fact cache 和控制器事件已经处理;废弃 collection 也可能仍被 execution environment 镜像或旧分支引用。Owner 应按周期审查依赖兼容、镜像漏洞、凭据轮换、inventory 漂移、失败重试模式、长期 ignore_errors、永久 no_log 黑洞和从未触发的 rollback。只有当团队既能稳定执行,也能解释失败、限制爆炸半径并完整退出,Ansible 才真正从个人脚本升级为资源编排能力。
