幂等性 — 跑两次也该什么都不发生
一句话总结
判断 playbook 质量的标准,不是第一次执行成功,而是第二次执行得到 changed=0。
为什么需要它
配置管理工具的真正价值,不是“自动安装”,而是**“随时确认当前状态是否符合期望,并把它调整到期望状态”。** 为此,它必须能在任意时刻安全执行。这样才能通过 cron 定期运行以纠正漂移,也不必担心部署流水线前一步失败后重新执行。
相反,非幂等 playbook 每次执行都会重启服务或向文件追加内容。人们会逐渐害怕运行它,而令人害怕的自动化最终不会再被使用。那一刻,基础设施就重新退回手工操作。
如何工作
幂等性由三种机制构成。
第一,了解状态的模块。 file、copy、template、lineinfile 等模块会读取当前状态,只修改差异;无须修改时报告 ok。
第二,shell 命令的 guard。 command/shell 无法自行幂等,必须附加条件。
| 机制 | 含义 |
|---|---|
creates: /path |
路径已存在则不执行 |
removes: /path |
路径不存在则不执行 |
changed_when: <조건> |
自行定义何时报告“已变更” |
failed_when: <조건> |
不依赖退出码,自行定义失败条件 |
changed_when: false 尤其常用,因为只查询状态的命令(例如查看版本)绝不属于变更。
第三,handler。 handler 是只有在“某项内容真实发生变化”时才执行的 task,常用于仅在配置文件变更后重新加载服务。
tasks:
- name: 설정 배치
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: reload app
handlers:
- name: reload app
ansible.builtin.command: /usr/bin/app-reload
有三条核心规则。(1) notify 只有在对应 task 状态为 changed 时才触发。(2) 即使多个 task 通知同一 handler,它也只执行一次。(3) handler 默认在一个 play 的所有 task 结束后执行。必须中途执行时使用 meta: flush_handlers。
第三条规则也带来陷阱:play 中途失败时,尚未执行的 handler 会直接消失,留下配置已变更、服务却未读取新配置的状态。此时可用 --force-handlers,即使失败也执行 handler。
在实际项目中
第一,“每次都重启的部署”。 原因几乎总是模板每次渲染出的结果不同,例如时间戳、随机值,或顺序不稳定的字典遍历。若渲染结果相同,template 模块会报告 ok,handler 也保持安静。
第二,真正的验证方法。 在 CI 中连续执行两次 playbook,检查第二次的 changed 与 failed 是否都为 0,是标准幂等性测试。必须让退出码判定,而不是依赖人工观察。
第三,幂等性与漂移。 幂等 playbook 本身就是漂移检测器。在被人手工修改的服务器上运行会出现 changed,这个数字就是“真实状态偏离代码的程度”。其思想与 Terraform 的 plan 完全相同。
如何让 changed 诚实
Ansible 中的幂等性,不仅是结果相同,还要求 changed 如实报告。没有任何变化却报告 changed,handler 会无谓执行;实际发生变化却报告 ok,服务会继续使用旧配置。
command 与 shell 总会报告 changed。 因为 Ansible 不知道命令做了什么,所以必须亲自说明何时运行,以及什么情况算作变更。
- name: 스키마 이전
ansible.builtin.command: /srv/app/migrate --apply
args:
creates: /srv/app/.migrated # 이 파일이 있으면 건너뛴다
register: migrate
changed_when: "'applied' in migrate.stdout"
failed_when: migrate.rc != 0 and 'already up to date' not in migrate.stderr
第一步是用 creates/removes 让不该执行的命令根本不执行;若必须执行,再用 changed_when 修正判定。
handler 在 play 末尾只执行一次。 即使通知十次,也只执行一次。这是优点,却也意味着:play 中途失败,handler 就完全不执行。 配置文件已经变化,daemon 却未重启;下一次执行时文件已经正确,只会报告 ok,handler 从此再也不会触发。--force-handlers 可以堵住这个缺口,更好的方式是另设 task,直接检查是否处于需要重启的状态。
写入配置前先验证。 写入错误配置后再重启,会让服务直接死亡。
- name: nginx 설정
ansible.builtin.template:
src: app.conf.j2
dest: /etc/nginx/conf.d/app.conf
validate: nginx -t -c %s # 통과해야 실제 자리에 놓는다
notify: reload nginx
先使用 --check 与 --diff 查看变化。但依赖前序 task 结果的 task 可能在 check mode 下失败或得出错误答案。这类只读 task 应设置 check_mode: false,让它在 check mode 下也真实读取。check mode 一旦开始说谎,人们就不会再使用,最终只能直接在生产环境运行。
下一实验要做什么
连续执行两次同一 playbook,让第一次产生变化、第二次得到 changed=0。确认 handler 只在第一次触发,并给 shell 命令添加 guard,确保执行两次后日志仍只有一行。最后生成幂等性判定报告。