在改动之前先看见将要改动什么
目标
把检查模式和 diff 当作“为每个 task 规定的行为”,而不是“两个标志”来处理。亲手制造虚假的失败并修复,还要创建用于附加到审批的计划产出和漂移门禁。
为什么重要
第一次把 playbook 运行到生产服务器上的那天,没有人知道这个 playbook 会对二十台机器做什么。--check 就是放在这个位置上的工具,但它会辜负你两次——检查模式一片干净而真正执行失败,检查模式红色报错而真正执行毫无问题。两次辜负的原因是相同的。检查模式是在不实际执行的情况下推测结果的模式,而推测的质量因模块而异。所以让检查模式变得可信,不是加上标志,而是给每个 task 规定“你在检查模式下要这样行动”。做完这件事之后,dry run 就不再只是部署前看的一幅图,而成为审批依据和抓住漂移的门禁。
步骤
- 创建
/root/anschk/hosts.ini——在[web]中放入web1(ansible_host=127.0.0.1、ansible_port=2222),并通过[all:vars]设置ansible_user=root。然后创建/root/anschk/site.yml:设置 play 变量app_env(默认lab)、app_dir_mode(默认"0755")、motd_owner(默认unset),用四个 task 完成以下事项:以app_dir_mode权限创建/root/anschk/app目录,以权限 0644 在/root/anschk/app/app.conf中写入env=<app_env>和listen=8080两行,以权限 0644 在/root/anschk/app/motd中写入Welcome=labhub和Owner=unset两行,最后把motd中的^Owner=行调整为Owner=<motd_owner>(请开启create,使该文件不存在时也会被创建——因为在检查模式下,前面的 task 并不会真的创建文件)。先不要收敛,只用--check --diff运行,把输出保存到/root/anschk/out/check1.txt。 - 把
site.yml加上--diff按默认值真正执行一次,把输出保存到/root/anschk/out/converge.txt。然后通过命令行覆盖三个值,再用--check --diff运行——app_env=stage、app_dir_mode=0750、motd_owner=platform-team。把该输出保存到/root/anschk/out/diff.txt。实际文件必须保持env=lab、权限 0755、Owner=unset不变。 - 创建
/root/anschk/probe.yml。用ansible.builtin.command运行getent passwd root,以pw进行 register(changed_when: false),并用ansible.builtin.debug把该值和当前是否处于检查模式,以check_mode=<참거짓> pwline=<읽은 값>(占位符依次为真假与读取到的值)的形式输出为一行。用--check运行这个 playbook,把输出连同标准错误一起保存到/root/anschk/out/probe-check.txt,并且在检查模式下也必须打印出真实的值。不能有任何一个被跳过的 task。 - 创建
/root/anschk/patch.yml——两个 task。一个以权限 0644 在/root/anschk/app/fresh.conf中写入env=lab和listen=9090两行,另一个把该文件的^listen=行改为listen=9443。先保持这个状态用--check运行,把失败输出连同标准错误保存到/root/anschk/out/false-failure.txt。然后给后一个 task 加上在检查模式下跳过的 guard,再次用--check运行,把该输出保存到/root/anschk/out/false-fixed.txt。两次都是检查模式,所以/root/anschk/app/fresh.conf到最后都不能被创建。 - 创建
/root/anschk/dryrun.yml。其中有一个以权限 0644 在/root/anschk/app/never.conf中写入一行feature=on的 task,它必须做到:即使不加--check、像平时一样运行,也绝不创建文件,只报告将会改变。在同一个 playbook 中再放一个 task——以权限 0644 在/root/anschk/app/dryrun-ran.txt中写入一行real-run的普通 task。不带任何标志运行这个 playbook,把输出保存到/root/anschk/out/dryrun.txt。结束后,/root/anschk/app/dryrun-ran.txt必须存在,而/root/anschk/app/never.conf必须不存在。 - 创建
/root/anschk/undef.yml——一个在内容中使用未定义变量(missing_var)的 copy task。再创建/root/anschk/badmod.yml——一个把模块名拼错为ansible.builtin.coppy的 task。然后创建/root/anschk/gates.sh,对这两个 playbook 分别依次运行--syntax-check、--list-tasks、--check,并把退出码逐行写入/root/anschk/out/gates.txt:<파일이름> syntax-check=<코드> list-tasks=<코드> check=<코드>(占位符依次为文件名与退出码)。请运行脚本,留下文件。 - 创建
/root/anschk/plan.sh。把ansible.posix.json指定为 stdout 回调,用--check --diff -e app_env=stage运行site.yml,把整个 JSON 保存到/root/anschk/out/plan.raw.json,然后从中只提取将会改变的 task 名称,以 JSON 数组保存到/root/anschk/out/plan.json。请运行脚本,留下这两个文件。数组中必须恰好只有一个名称。 - 创建
/root/anschk/drift-gate.sh。用检查模式运行site.yml,如果摘要中的changed为 0,就输出首行以CLEAN开头的消息并以 0 结束;如果为 1 以上,就输出首行以DRIFT开头的消息并以 1 结束。如果检查模式的运行本身失败,就输出以DRIFT-UNKNOWN开头的消息并以 1 结束。传给脚本的参数必须原样传给ansible-playbook。把在已收敛状态下不带参数运行的输出保存到/root/anschk/out/gate-clean.txt,把加上-e app_env=stage运行的输出保存到/root/anschk/out/gate-dirty.txt。
参考
- 请先在第 1 步创建 inventory 和 playbook。这个 Pod 的 sshd 运行在 127.0.0.1:2222,并且已经完成密钥认证。
- 命令提示:
ansible-playbook -i hosts.ini site.yml --check --diff是基本工具,用ansible-doc -t callback -l查看可用的回调。输出用> 파일 2>&1(占位符为文件名)连同标准错误一起接收。 - 命令提示:魔法变量
ansible_check_mode会告诉你当前是否处于检查模式。加在 task 上的check_mode键,会让那个 task 单独脱离检查模式(false)或被锁在检查模式之内(true)。 - 常见错误:不知道警告和错误是从标准错误输出的,只写了
> 파일(占位符为文件名),结果留下一个空文件。 - 常见错误:在保存以失败结束的命令的输出时,因为
set -e而让脚本在那里停止。 - 常见错误:把
check_mode: false加到会改变目标的 task 上,让 dry run 本身变成谎言。 - 这个 Pod 没有审计日志和外部审批系统,所以审批流程只讲到把计划产出保存为文件为止。也没有 capability,所以不处理服务重启之类的 task,只使用文件和目录。
- 用检查模式验证 · ansible-playbook 选项 · lineinfile 模块 · copy 模块 · 条件判断
一创建就先用检查模式看
创建 /root/anschk/hosts.ini——在 [web] 中放入 web1(ansible_host=127.0.0.1、ansible_port=2222),并通过 [all:vars] 设置 ansible_user=root。然后创建 /root/anschk/site.yml:设置 play 变量 app_env(默认 lab)、app_dir_mode(默认 "0755")、motd_owner(默认 unset),用四个 task 完成以下事项:以 app_dir_mode 权限创建 /root/anschk/app 目录,以权限 0644 在 /root/anschk/app/app.conf 中写入 env=<app_env> 和 listen=8080 两行,以权限 0644 在 /root/anschk/app/motd 中写入 Welcome=labhub 和 Owner=unset 两行,最后把 motd 中的 ^Owner= 行调整为 Owner=<motd_owner>(请开启 create,使该文件不存在时也会被创建——因为在检查模式下,前面的 task 并不会真的创建文件)。先不要收敛,只用 --check --diff 运行,把输出保存到 /root/anschk/out/check1.txt。
检查模式不对目标做出改变,如果看起来会发生变更,就把那个 task 报告为 changed。所以在还什么都没有的状态下运行,所有将会创建的内容都会显示为 changed。如果同时加上 --diff,将要生成的文件内容会以 + 行显示,而新生成文件的 diff,其前面的范围会显示为 0——这个标记就成了“当时确实没有这个文件”的证据。输出最好同时接收标准输出和标准错误。
收敛一次,改动三个值,看三种 diff
把 site.yml 加上 --diff 按默认值真正执行一次,把输出保存到 /root/anschk/out/converge.txt。然后通过命令行覆盖三个值,再用 --check --diff 运行——app_env=stage、app_dir_mode=0750、motd_owner=platform-team。把该输出保存到 /root/anschk/out/diff.txt。实际文件必须保持 env=lab、权限 0755、Owner=unset 不变。
--diff 并不只限检查模式——加在真正的执行上,会在改变的同时显示改变了什么。这两种用法在同一步里并排查看。改动三个值,就会出现三种 diff:文件内容改变的 diff、改变的不是文件内容而只是属性、比较的是 JSON 的 diff,以及只改变一行的 diff。命令行变量优先于 play 变量,所以传三次 -e 이름=값(韩文,意为“名称=值”)即可。最后别忘了用眼睛确认实际文件。
让检查模式下被跳过的查询 task 恢复
创建 /root/anschk/probe.yml。用 ansible.builtin.command 运行 getent passwd root,以 pw 进行 register(changed_when: false),并用 ansible.builtin.debug 把该值和当前是否处于检查模式,以 check_mode=<참거짓> pwline=<읽은 값>(占位符依次为真假与读取到的值)的形式输出为一行。用 --check 运行这个 playbook,把输出连同标准错误一起保存到 /root/anschk/out/probe-check.txt,并且在检查模式下也必须打印出真实的值。不能有任何一个被跳过的 task。
是否支持检查模式由模块决定。命令模块不知道那条命令会做什么,所以不支持,而不支持的模块所属的 task 就会直接被跳过。一跳过,register 变量就是空的,后面的 task 就会崩溃——请先原样运行一次,亲眼看到 pwline= 后面是空的。让它恢复的键是加在 task 上的一行,加的标准只有一个:你是否确信这个 task 不会改变目标。
制造检查模式的虚假失败,并用 guard 修复
创建 /root/anschk/patch.yml——两个 task。一个以权限 0644 在 /root/anschk/app/fresh.conf 中写入 env=lab 和 listen=9090 两行,另一个把该文件的 ^listen= 行改为 listen=9443。先保持这个状态用 --check 运行,把失败输出连同标准错误保存到 /root/anschk/out/false-failure.txt。然后给后一个 task 加上在检查模式下跳过的 guard,再次用 --check 运行,把该输出保存到 /root/anschk/out/false-fixed.txt。两次都是检查模式,所以 /root/anschk/app/fresh.conf 到最后都不能被创建。
在检查模式下,前面的 task 并不会真的创建文件。所以以为那个文件存在的后一个 task,会因为要去修改不存在的文件而失败——并不是 playbook 错了,而是检查模式的局限。失败消息中会原样写出这一点,请先读一读。常见的修复方法是给后一个 task 加一个条件,而当前是否处于检查模式,由一个魔法变量告知。虽然也可以给前一个 task 加上 check_mode: false,但那一刻就不再是 dry run 了,所以这一步不用。
创建一个即使在实际执行中也绝不改变的 task
创建 /root/anschk/dryrun.yml。其中有一个以权限 0644 在 /root/anschk/app/never.conf 中写入一行 feature=on 的 task,它必须做到:即使不加 --check、像平时一样运行,也绝不创建文件,只报告将会改变。在同一个 playbook 中再放一个 task——以权限 0644 在 /root/anschk/app/dryrun-ran.txt 中写入一行 real-run 的普通 task。不带任何标志运行这个 playbook,把输出保存到 /root/anschk/out/dryrun.txt。结束后,/root/anschk/app/dryrun-ran.txt 必须存在,而 /root/anschk/app/never.conf 必须不存在。
这个键与前一步使用的键同名,只是值相反。把这个键加到 task 上,那个 task 就始终被固定为 dry run。当想把暂时还不能开启的 task 预先写进 playbook、只看其影响,或者想把危险的 task 锁住、直到有人确认之前不执行时,就用它。“被报告为 changed,文件却不存在”这种别扭的状态,正是这个键所做的事。
把三个工具的退出码在哪里出现分歧整理成表
创建 /root/anschk/undef.yml——一个在内容中使用未定义变量(missing_var)的 copy task。再创建 /root/anschk/badmod.yml——一个把模块名拼错为 ansible.builtin.coppy 的 task。然后创建 /root/anschk/gates.sh,对这两个 playbook 分别依次运行 --syntax-check、--list-tasks、--check,并把退出码逐行写入 /root/anschk/out/gates.txt:<파일이름> syntax-check=<코드> list-tasks=<코드> check=<코드>(占位符依次为文件名与退出码)。请运行脚本,留下文件。
前面两个工具既不连接目标,也不展开模板。变量要在 task 真正以检查模式执行时才会被展开——所以两个 playbook 的结果会以不同的形态出现分歧。哪一个在哪里最早被抓到,就是这一步的答案。退出码要在命令之后马上用 $? 接收。输出丢弃、只留下退出码即可。编写 CI 时,这张表决定了顺序——从成本低的开始设置,原因就在这里。
创建用于附加到审批的计划产出
创建 /root/anschk/plan.sh。把 ansible.posix.json 指定为 stdout 回调,用 --check --diff -e app_env=stage 运行 site.yml,把整个 JSON 保存到 /root/anschk/out/plan.raw.json,然后从中只提取将会改变的 task 名称,以 JSON 数组保存到 /root/anschk/out/plan.json。请运行脚本,留下这两个文件。数组中必须恰好只有一个名称。
屏幕上滚过的绿色、黄色文字不会作为审批依据保留下来。只要改变回调,整个运行就会输出为一个结构化的 JSON——用环境变量 ANSIBLE_STDOUT_CALLBACK 指定。有哪些回调,用 ansible-doc -t callback -l 查看。在 JSON 中,task 位于 .plays[].tasks[] 之下,task 名称是 .task.name,各主机的结果在 .hosts 之下。因为只改了 app_env,所以将会改变的 task 只有一个——其余的已经是那个状态,所以不是 changed。
把检查模式变成门禁
创建 /root/anschk/drift-gate.sh。用检查模式运行 site.yml,如果摘要中的 changed 为 0,就输出首行以 CLEAN 开头的消息并以 0 结束;如果为 1 以上,就输出首行以 DRIFT 开头的消息并以 1 结束。如果检查模式的运行本身失败,就输出以 DRIFT-UNKNOWN 开头的消息并以 1 结束。传给脚本的参数必须原样传给 ansible-playbook。把在已收敛状态下不带参数运行的输出保存到 /root/anschk/out/gate-clean.txt,把加上 -e app_env=stage 运行的输出保存到 /root/anschk/out/gate-dirty.txt。
如果收敛结束之后,检查模式仍然找出了将会改变的项,就意味着有人手工改过,或者 playbook 自己无法收敛——把在这种条件下以失败结束的脚本挂到 CI 上,漂移在下一次部署之前就会暴露出来。从摘要行中提取数字时,看 changed= 后面的数即可,而运行失败、根本没有摘要的情况也要单独处理——如果没读到数字却悄悄放行,那就不是门禁,而是摆设。原样传递脚本参数的方法是 "$@"。由于门禁会以失败结束,所以在保存输出时,请不要让 shell 在那里停止。