LabHub
学习 学习路径 课程

Ansible 基础

幂等性 — 跑两次也该什么都不发生

在 LabHub 中继续学习

一句话总结

判断 playbook 质量的标准,不是第一次执行成功,而是第二次执行得到 changed=0

概念图: 第二次执行得到 changed=0。 · “随时确认当前状态是否符合期望,并把它调整到期望状态”。 · 第一,了解状态的模块。 · 第二,shell 命令的 guard。

为什么需要它

配置管理工具的真正价值,不是“自动安装”,而是**“随时确认当前状态是否符合期望,并把它调整到期望状态”。** 为此,它必须能在任意时刻安全执行。这样才能通过 cron 定期运行以纠正漂移,也不必担心部署流水线前一步失败后重新执行。

相反,非幂等 playbook 每次执行都会重启服务或向文件追加内容。人们会逐渐害怕运行它,而令人害怕的自动化最终不会再被使用。那一刻,基础设施就重新退回手工操作。

如何工作

幂等性由三种机制构成。

第一,了解状态的模块。 filecopytemplatelineinfile 等模块会读取当前状态,只修改差异;无须修改时报告 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,检查第二次的 changedfailed 是否都为 0,是标准幂等性测试。必须让退出码判定,而不是依赖人工观察。

第三,幂等性与漂移。 幂等 playbook 本身就是漂移检测器。在被人手工修改的服务器上运行会出现 changed,这个数字就是“真实状态偏离代码的程度”。其思想与 Terraform 的 plan 完全相同。

如何让 changed 诚实

Ansible 中的幂等性,不仅是结果相同,还要求 changed 如实报告。没有任何变化却报告 changed,handler 会无谓执行;实际发生变化却报告 ok,服务会继续使用旧配置。

commandshell 总会报告 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,确保执行两次后日志仍只有一行。最后生成幂等性判定报告。