幂等、预览与滚动变更
本章目标:让编排具备生产变更所需的三种能力——多跑无害(幂等)、先看再改(check/diff)、控制爆炸半径(serial/失败阈值)。
1. 幂等:定义与自测
对同一批主机、同一份 playbook:
| 次数 | 期望 |
|---|---|
| 第 1 次 | 可能有若干 changed,达到目标状态 |
| 第 2 次 | 几乎全是 ok / skipped,服务不被无故重启 |
| 第 N 次 | 与第 2 次相同 |
自测:
ansible-playbook playbooks/web.yml -l web1 | tee /tmp/run1.log
ansible-playbook playbooks/web.yml -l web1 | tee /tmp/run2.log
# 对比 changed 数量
grep -c changed /tmp/run1.log
grep -c changed /tmp/run2.log
破坏幂等的常见写法
| 写法 | 问题 | 改法 |
|---|---|---|
每轮 state: restarted | 总是变更 | handler + 配置 changed 才重启 |
shell: echo x >> file | 重复追加 | lineinfile / template |
state: latest 装包 | 版本漂 | present + 钉版本 |
| 用时间戳写文件内容 | 次次不同 | 内容由稳定变量生成 |
无 changed_when 的 command | 总是 changed | 声明 changed_when: false 或准确条件 |
用 check_mode 辅助设计
编写角色时,想清楚每个 task 在 check mode 下是否合理。对「必须真实执行」的只读探测:
- name: 读取当前内核参数
ansible.builtin.command: sysctl net.ipv4.ip_forward
register: sysctl_out
changed_when: false
check_mode: false
2. check mode:预览
ansible-playbook playbooks/web.yml --check
ansible-playbook playbooks/web.yml --check --diff
含义:
- 多数内置模块会模拟「若执行会怎样」
- 不会保证 100% 与真实 apply 一致(尤其是依赖前序真实产物、下载、自定义脚本)
--diff对 file/template/copy 特别有用,能直接看配置差异
推荐流程:
1. --check --diff -l 一台灰度机
2. 真实 apply -l 同一台
3. 业务/健康检查
4. serial 分批扩大
哪些任务在 check 下不准
command/shell默认常被跳过或结果不可靠- 需要先安装包再 template validate 的链路
- 依赖外部 API 返回值的逻辑
因此:check 通过 ≠ 可以闭眼全量,它是必要但不充分条件。
3. diff:把变更讲给人听
ansible-playbook playbooks/web.yml -l web1 --check --diff
输出中关注:
- 哪些文件将变更
- 删除/新增了哪些关键行(upstream、监听端口、鉴权)
- 是否误扫到本不该管的文件
模板顶部加头注释,方便人工识别:
# {{ ansible_managed }}
# role=nginx source=templates/nginx.conf.j2
ansible_managed 默认含路径与时间等信息,可在 ansible.cfg 定制:
[defaults]
ansible_managed = Managed by Ansible - do not edit on server
4. 滚动与批次:serial
全量并行 reload/restart 是事故温床。
- name: 滚动 Web
hosts: web
become: true
serial: 1
max_fail_percentage: 25
any_errors_fatal: false
roles:
- nginx
| 写法 | 含义 |
|---|---|
serial: 1 | 一次一台 |
serial: "30%" | 每批约 30% |
serial: [1, 2, 5] | 灰度节奏:先 1,再 2,再 5… |
max_fail_percentage | 失败占比超阈值,中止后续批次 |
any_errors_fatal: true | 任一主机失败则整体快速失败 |
批次间插入验证
- name: 滚动并验证
hosts: web
become: true
serial: 1
roles:
- nginx
tasks:
- name: flush handlers 以便立刻 reload
ansible.builtin.meta: flush_handlers
- name: 本机健康检查
ansible.builtin.uri:
url: "http://127.0.0.1:{{ nginx_listen_port | default(80) }}/healthz"
status_code: 200
register: health
retries: 5
delay: 2
until: health.status == 200
retries 只有和 until 一起使用才会真正轮询。健康检查在次数耗尽后仍失败 → 该主机失败 → 触发 max_fail_percentage 保护,避免继续推坏配置。
5. 限流与并发相关旋钮
# ansible.cfg
[defaults]
forks = 10
# 临时
ansible-playbook playbooks/site.yml --forks 5
| 旋钮 | 作用 |
|---|---|
forks | 全局并行度 |
serial | 每 play 批次 |
throttle(task 级) | 限制某任务并发 |
- name: 对外部 API 温和一点
ansible.builtin.uri:
url: https://api.example.com/reload
method: POST
throttle: 3
6. 失败策略与排障
- name: 允许部分失败继续(谨慎)
ansible.builtin.command: /usr/local/bin/optional-check
register: opt
failed_when: false
changed_when: false
- name: 明确失败条件
ansible.builtin.command: /usr/local/bin/must-pass
register: must
failed_when: must.rc != 0
从失败主机续跑:
ansible-playbook playbooks/web.yml --limit @/path/to/playbook.retry
(需未关闭 retry 文件;我们在 cfg 里曾关过 retry_files_enabled,生产可按需打开。)
更推荐:用 -l 显式列出失败主机重跑,并把失败原因记进变更单。
7. 变更窗口检查清单
上线前打印出来逐项勾:
- 目标:
-i与-l是否指向预期环境? - 预览:是否做过
--check --diff? - 幂等:灰度机是否二次 apply 无多余 changed?
- 批次:
serial是否设置?是否有批次间健康检查? - 回滚:Git 是否可 revert?关键文件是否
backup: true? - 密钥:Vault 密码是否仅在本窗口可用?
- 观察:指标/日志面板是否打开?
8. 完整示例:带护栏的 web play
playbooks/web_safe.yml:
---
- name: Safe web rollout
hosts: web
become: true
serial: [1, "30%", "100%"]
max_fail_percentage: 30
pre_tasks:
- name: 拒绝空组误跑
ansible.builtin.assert:
that:
- groups['web'] | length > 0
fail_msg: "web 组为空,停止"
roles:
- role: nginx
tasks:
- name: 立即应用 handler
ansible.builtin.meta: flush_handlers
- name: 健康检查
ansible.builtin.uri:
url: "http://127.0.0.1:{{ nginx_listen_port }}/healthz"
status_code: 200
register: hz
retries: 6
delay: 2
until: hz.status == 200
ansible-playbook playbooks/web_safe.yml -i inventory/staging.ini --check --diff
ansible-playbook playbooks/web_safe.yml -i inventory/staging.ini
9. 练习与验收
练习 A
给当前 nginx 角色连续 apply 两次,统计 changed;若第二次仍有 changed,找出并修掉。
练习 B
改一个监听端口,先 --check --diff 截图/保存输出,再真实 apply。
练习 C
serial: 1 跑两台主机,人为让第一台健康检查失败(改错 port),确认第二台不会被继续变更(配合 max_fail_percentage / 失败策略观察行为)。
验收清单
- 能解释幂等被破坏的三种典型原因
- 变更默认走 check → 灰度 → 分批
- 会为 reload 类变更写健康检查
下一章:Vault 与密钥 —— 护栏齐了,还要把密码从明文仓库里拿掉。