TT Lab
开始
学习 学习路径 课程

Ansible 基础

检查模式能模拟什么,不能模拟什么

在 TT Lab 中继续学习

一句话总结

--check 是用来询问“现在运行会改变什么”的模式,而它给出答案的准确度因模块而异。让检查模式变得可信,不是加上一个标志就行,而是要为每个 task 规定它在检查模式下如何行动。

为什么需要它

第一次把 playbook 运行到生产服务器上的那天,恐惧是具体的。没有人知道这个 playbook 会对二十台机器做什么。可能有人手工改过的配置,也可能有半年前的人加入的 task 现在会做出莫名其妙的事。可是又不能一台一台用眼睛看着过去。

--check 就是放在这个位置上的工具。官方文档把这个模式称为 dry run,并用一句话定义了它所做的事。不对远程系统做出改变,并且如果看起来会发生变更,就把那个 task 报告为 changed。如果同时加上 --diff,还会逐行显示具体有什么不同。

然而这个工具会辜负你两次。一次是虚假的安心——检查模式下一片干净,真正运行时却失败。另一次是虚假的失败——检查模式下红色报错,真正运行时却毫无问题。两次辜负的原因是相同的。检查模式是在不实际执行的情况下推测结果的模式,而推测的质量因模块而异。

工作原理

检查模式是逐个 task 分别决定的。如果模块支持检查模式,就会报告“将会改变”,而不是真的去改变。如果不支持,那个 task 就会被跳过(skipping)。command 和 shell 就是典型——Ansible 不知道那条命令会做什么,所以干脆不运行。

第一起事故就出在这里。只做查询的命令 task 一旦被跳过,它的 register 变量就是空的,使用这个值的后续 task 会接连崩溃。所以对只读取的 task,要加上 check_mode: false。这个 task 在检查模式下也会被真正执行。

- name: 현재 설치된 판을 읽는다
  ansible.builtin.command: myapp --version
  register: current
  changed_when: false
  check_mode: false

加的标准只有一个。这个 task 会不会改变目标。只有在确信不会改变时才加。如果把 check_mode: false 加到会改变目标的 task 上,那个 task 在 dry run 中也会被真正执行,而在那一刻,dry run 就成了谎言。

也有相反的方向。加了 check_mode: true 的 task,平时也绝不会改变。在实际执行中,它同样只报告“将会改变”。当想把暂时还不能开启的 task 预先写好、只看其影响,或者想把危险的 task 锁住、直到有人确认之前不执行时,就使用它。

虚假的失败是由顺序造成的。想象一个 playbook:前一个 task 创建文件,后一个 task 修改那个文件。在检查模式下,前一个 task 并没有真的创建文件,所以后一个 task 会因为要去修改一个不存在的文件而失败。在这个实验环境中,lineinfile 恰好会以 Destination ... does not exist ! 失败。并不是 playbook 错了,而是检查模式的局限。

修复方法有三种,价值各不相同。第一,给后一个 task 加上 when: not ansible_check_mode,在检查模式下跳过——这是最常见也最诚实的做法。代价是那个 task 会改变什么,就从计划中消失了。第二,给前一个 task 加上 check_mode: false,让它真的创建——这样就不再是 dry run,所以很难说值得推荐。第三,从一开始就改变设计,让一个模块整体管理那个文件——这是最好的,但也是最大的改动。

--diff 的格式是固定的。在 --- before 和 +++ after 之下,变化的行以 - 和 + 显示。如果改变的不是内容而只是属性,显示的就不是文件内容,而是属性 JSON 的 diff(比如目录的 state: absent 变成 state: directory 这样)。--diff 并不只限检查模式——加在真正的执行上,就会在改变的同时显示改变了什么。在事故调查中,这一种往往更有用。

对于包含机密的文件,要同时加上 no_log: true。否则 diff 会把那个值原样打印到日志里。

三个工具的位置各不相同。

工具 做什么 会连接目标吗 会展开模板吗
--syntax-check 读取 YAML 和 playbook 的结构 不会 不会
--list-tasks 只列出本次选择会运行什么 不会 不会
--check 以检查模式真正执行 task 会 会

实测得到的差别把这一点说得很清楚。使用未定义变量的 playbook,会以退出码 0 通过 --syntax-check 和 --list-tasks。那个错误要到 --check 才会第一次暴露,退出码为 2。反过来,模块名拼写错误或缩进事故,三个工具都会以退出码 4 抓到。所以 CI 会按顺序设置这三个——从成本低的开始。

把检查模式用作审批流程时,需要人可以阅读的产出。屏幕上滚过的绿色、黄色文字不会作为审批依据保留下来。如果把 ansible.posix.json 回调指定为 stdout 回调,整个运行会输出为一个 JSON,从中只提取“将会改变的 task 名称列表”,就可以附到变更请求上。

ANSIBLE_STDOUT_CALLBACK=ansible.posix.json \
  ansible-playbook -i hosts.ini site.yml --check --diff > plan.json

再进一步,就成了门禁。在收敛结束之后,再用检查模式运行一次,只要有一个将会改变的项,就以失败结束——把这样的脚本挂到 CI 上,被人手工改过的服务器,在下一次部署之前就会暴露出来。这就是检查模式从“观察的工具”变成“守护的工具”的地方。

在现场相遇的样子

第一,检查模式一片干净,真正执行却失败。多半是因为 shell task 被跳过了。如果检查模式下有很多 skipped,说明那个 playbook 的 dry run 就少看了这么多。养成把 PLAY RECAP 中的 skipped 数字当作计划可信度来读的习惯,会有帮助。

第二,检查模式是红色的,实际上却毫无问题。刚开始引入的团队,最多就是在这里放弃。他们得出“我们的 playbook 不能用 --check”的结论,从此完全不用 dry run。实际上,只要给两三个 task 加上 guard 就解决了。

第三,diff 把机密泄漏到日志里。用模板部署证书密钥或数据库密码的同时加上 --diff,CI 日志里就会原样留下这些值。最好从一开始就确立加上 no_log: true 的规则。

第四,审批是靠屏幕截图来回传递的。把 dry run 结果作为图片贴进去的团队,半年后找不到“当时批准了什么”。只要有一份 JSON 计划产出,审批历史就会以可搜索的文本形式保留下来。

第五,这个实验环境的如实局限。这个 Pod 中没有审计日志,也没有外部审批系统。所以“审批流程”只讲到把计划产出保存为文件为止,把这个文件贴到哪里,只在阅读中讨论。服务重启之类的 task 也因为没有 capability 而无法处理,所以检查模式的对象限定为文件和目录。

参考文档

下一项实验要做什么

刚创建好 playbook,就先用 --check 运行,确认什么都没有生成,收敛一次之后,改动三个值,用 --check --diff 取出内容、权限、单行这三种 diff。看到只读命令 task 在检查模式下被跳过,就用 check_mode: false 让它恢复;反过来,用 check_mode: true 创建一个即使在实际执行中也绝不改变的 task。故意制造检查模式的虚假失败,并用 ansible_check_mode guard 修复。对使用未定义变量的 playbook 依次运行 --syntax-check、--list-tasks、--check,把退出码最早在哪里出现分歧整理成表,用 JSON 回调生成用于审批的计划产出,然后亲手编写在有将会改变的项时以 1 结束的漂移门禁脚本。