Ansible --check 和 --diff 预演到底能证明什么
详解 Ansible --check 与 --diff 的真实含义:哪些任务会显示 changed、哪些任务会被跳过,以及为何不支持检查模式的模块会让预演结果与正式执行不一致。
Ansible 检查模式的作用
Ansible 检查模式是一种试运行:ansible-playbook --check 会连接 play 中的每台主机,询问每个模块当前状态是否已经符合指定状态,并报告将发生哪些更改,但不会写入任何内容。添加 --diff 后,它还会输出将要修改的文件内容,包括修改前和修改后的内容。两者结合,可以回答每次正式运行前都应确认的问题:这些服务器即将发生哪些更改?
检查模式不是对 playbook 的模拟。系统中不存在服务器模型。每个模块只会被要求执行读取操作,而不是写入操作。能够以只读方式回答的模块会报告 changed,然后继续执行。无法回答的模块不会执行任何操作,也不会报告任何内容。Ansible 文档用一句话概括了这一点:“不支持检查模式的模块不会报告任何内容,也不会执行任何操作。”这正是试运行可能给出错误结果的地方,因此本指南的大部分内容都围绕这一差距展开。
运行预演:--check 和 --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C 和 -D 是这两个标志的简写形式。这里特意使用了 --limit。查看一台主机的差异尚可接受。查看 20 台主机的差异,则通常只能不断滚动屏幕。
四个结果词涵盖了整份报告。
ok: [web1]表示模块已检查,当前状态已经符合要求。不会发生任何更改。changed: [web1]表示模块将写入内容。使用--diff时,其上方的行会显示具体更改。skipping: [web1]表示未评估该任务。原因可能是when的值为 false,也可能是模块无法在检查模式下运行。fatal: [web1]表示检查任务失败。先阅读错误消息,再判断 playbook 是否损坏。
对于文件模块,--diff 会输出统一差异。删除的行标记为 -,新增的行标记为 +。差异位于一个标头下方,标头中的行以 --- before 和 +++ after 开头,并标明目标路径。不写入文件的模块会输出自己的前后状态,因此 ansible.builtin.user 显示的是将要更改的属性,而不是文件内容。
在 ansible.cfg 中永久启用差异显示,这样就不会忘记该标志:
[diff]
always = true
context = 5检查模式之前还可以执行两项开销更低的检查。ansible-playbook site.yml --syntax-check 会解析 YAML 和 play 结构,且不会联系任何主机。ansible-playbook site.yml --list-tasks 会输出将要运行的任务。借此可以发现某个 role 实际上没有使用你以为已设置的标签。这两项检查都不会建立连接,因此会立即完成。
检查模式本身会建立连接。它会通过 SSH 连接模式中的每台主机并收集事实,因此主机离线时,预演会失败。这本身就是有用的信号;也正因如此,在将预演加入 CI 前,需要先明确 确定 playbook 应如何处理无法访问的主机。
为何在全新服务器上检查模式会失败
此 play 是正确的。对尚未安装 nginx 的服务器使用 --check 运行它时,其中大部分任务都会失败。
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueapt 任务报告 changed,这是正确的:软件包不存在,因此实际运行时会安装它。检查模式不会安装软件包。随后,template 任务失败,因为此主机上不存在 /etc/nginx/conf.d/,也没有任务创建它。service 任务同样失败,因为没有可供查询的 nginx 单元。这些失败都不是 playbook 的错误。试运行缺少所需的主机状态;文档所说的“对于输入依赖前一个任务变更的任务,检查模式无法生成有用输出”,指的就是这种情况。
因此,更准确的规则是:对于 playbook 已完成收敛的主机,检查模式能够准确反映结果;对于全新的主机,输出会包含大量失败信息。如果对已完成收敛的主机执行 --check,并且每个任务都报告 ok,这是真实的状态说明,因为它表示不会发生任何变更。在全新的主机上,--check 主要说明该主机是全新的。针对 VPS 编写 第一个 Ansible playbook 时,应预期第一次试运行会显示大量红色错误,并根据第二次试运行评估 playbook。
为什么在检查模式下会跳过命令和 Shell 任务
ansible.builtin.command 和 ansible.builtin.shell 无法判断命令的作用。没有一种只读方式可以运行任意二进制文件,因此模块在检查模式下会拒绝运行该命令。任务结果会包含 skipped: true,消息为 Command would have run if not in check mode,输出中会显示 skipping: [web1]。
模块文档将其对检查模式的支持称为“部分支持”,并指出可使用 creates 和 removes 作为变通方法。为任务指定 creates 路径后,检查模式至少可以评估文件测试:
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/app如果 /opt/app/bin/app 已存在,检查模式会报告 Would not run command since '/opt/app/bin/app' exists,这是一个有效结果。如果路径不存在,则会得到 Command would have run if not in check mode,这同样是一个有效结果。如果没有 creates,该任务在试运行中就不会提供任何结果。
跳过任务带来的连锁影响比没有结果更严重。被跳过的任务仍会注册结果,但该结果是跳过结果,不包含 stdout 键。评估下一个任务的条件时会失败,并显示类似 'dict object' has no attribute 'stdout' 的错误。这样,playbook 在实际运行中可以正常工作,却会在试运行中失败,这是整个功能中最容易造成混淆的问题。
check_mode:false,以及它唯一适用的位置
任务中的 check_mode: false 表示“即使在 --check 下,也实际执行此任务”。它用于修复命令被跳过的问题,但只有读取数据的任务才能安全使用。
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: false该任务在两种模式下都能准确反映结果。它读取版本且从不写入,changed_when: false 可避免任务报告未实际执行的更改,check_mode: false 则会在试运行期间创建 app_version.stdout,因此基于它构建的条件仍能得到正确计算。
在将此关键字复制到其他位置前,请先按字面理解其含义。带有 check_mode: false 的任务会在 ansible-playbook --check 期间写入服务器。将其添加到 apt 任务或 template 任务,只会让试运行看起来更简洁,而此时它已经不再是试运行。无法确保写入任务安全时,应改为使用条件保护:
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode 是 Ansible 设置的特殊变量。在检查运行期间,其值为 true。反向关键字也存在。check_mode: true 会始终将任务固定为检查模式,即使在实际运行期间也是如此,因此可将任务变成配置偏差探测器:注册任务结果后,changed 报告表示主机已不再符合任务要求的状态。
为什么任务每次运行都报告 changed
连续运行两次 playbook,中间不做任何操作。第二次运行时,每个任务都应报告 ok。如果某个任务仍报告 changed,说明原因只有两种:模块无法看到它负责管理的状态,或者传入的输入不稳定。这两种问题都可以修复,不能简单静默处理。
command和shell未设置creates、removes或changed_when时,每次都会报告changed,因为模块无法判断是否发生过变化。添加creates,或者针对输出中的字符串设置changed_when。ansible.builtin.file配合state: touch时,每次运行都会按设计报告changed,因为修改文件会更新其时间戳。如果你的目标只是设置所有者或权限,请使用state: file。- 如果
template的渲染输出发生变化,文件就会在每次运行时被重写。来自ansible_date_time的时间戳、对now()的调用,或每次重新生成的密码,都会产生不同的字节,因此模块正确地报告了变化。将这个变化值移出模板。 ansible.builtin.user配合password: "{{ pw | password_hash('sha512') }}"时,每次运行都会发生变化,因为password_hash每次调用都会随机生成 salt,所以生成的哈希值永远不会匹配/etc/shadow中已有的值。传入一个根据稳定数据生成的显式 salt。- 软件包模块上的
state: latest在有可用升级时会报告changed。这符合预期。也正因如此,state: latest生成的 playbook 结果无法预测。使用state: present,并按需执行升级。 ansible.builtin.unarchive指向没有creates的 URL 时,会重复下载并重新解压。为它提供一个creates路径。
--diff 是区分这些情况的最快方法。如果任务报告 changed,且 diff 显示字节内容发生变化,说明输入不稳定。如果任务报告 changed,而 diff 完全没有内容,说明模块无法表达它所执行的更改。这通常表示任务属于 command,或者只是执行了时间戳之类的元数据写入。
不要使用 changed_when: false 来静默处理产生噪声的任务。它会抑制报告,因此 notify 永远不会触发,负责重启服务的 handler 也不会运行。应直接修复任务。
缩小影响范围:--limit、--tags 和 --step
检查模式会告诉您哪些内容将发生变化。这些选项决定一次有多少台机器执行这些变更。
--limit 将 play 限制为 inventory 的一部分。它接受与 hosts: 相同的模式,因此 --limit web1 和 --limit 'webservers:!web3' 都有效。请将模式加引号。在交互式 bash 会话中,未加引号的 ! 会触发感叹号上的历史展开,shell 会在 Ansible 看到命令前改写它。
在信任模式前先确认它。ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts 会打印匹配的主机,然后退出,不连接其中任何主机。没有匹配项的模式是安全的,因为 Ansible 不会回退到整个 inventory。它会警告无法匹配主机模式,然后退出,并显示主机和 --limit 与任何主机都不匹配。首先了解inventory 文件如何定义这些组,才能准确预测模式的匹配结果。
--tags deploy 只运行带有指定标签的任务,--skip-tags packages 则运行其他所有任务。--list-tags 会打印可用的标签。当 play 超出您愿意全部运行的范围后,标签才真正有用;这也是将长 playbook 拆分为多个 role的原因之一。
--start-at-task "Write the site config" 从指定任务恢复失败的运行。使用它进行恢复时,请了解其代价:该任务之前的所有内容都会被跳过,包括设置 fact 或注册变量的任务,而后续任务可能会读取这些变量。
--step 会在每个任务前提示,并等待您回答 yes、no 或 continue。它的执行速度较慢,但第一次运行具有破坏性的操作时应使用它。这样您可以在两个任务之间停止,而不是运行二十个任务后才停止。
使用 serial 分批发布变更
默认情况下,Ansible 会先对 play 中的每台主机执行一个任务,然后再开始下一个任务。这样执行速度很快,但也意味着错误任务会在同一秒内到达整个主机群。在您读到错误并按下 Ctrl-C 之前,变更可能已经应用到所有主机。
serial会将 play 拆分为多个批次。整个 play 会先对第一个批次执行,然后再处理下一个批次。
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webapp第一个批次只有1台主机。如果该主机运行正常,第二个批次包含5台主机,之后的每个批次包含 play 中主机总数的30%。max_fail_percentage: 0会在某个批次中的任意主机失败后立即结束 play,因此有问题的版本只会应用到1台机器。any_errors_fatal: true的行为更直接:第1台主机失败后,立即为所有主机结束 play。
先对1台主机执行并不是多虑,原因很明确。Inventory 中的主机组会逐渐出现差异。某台服务器可能比其他服务器晚6个月加入,因此运行不同的发行版版本,或包含某人手动安装的服务,或采用不同的磁盘布局。对于整个主机组来说,playbook 可能是正确的,但对这1台主机却不适用;对配置一致的主机执行 dry run 也无法发现这种问题。管理 Linux 服务器群很大程度上就是在变更应用前找出异常主机。
运行顺序
ansible-playbook site.yml --syntax-check无需网络即可检查 YAML 和结构错误。ansible-playbook site.yml --limit web1 --list-hosts可验证您的模式匹配结果是否符合预期。ansible-playbook site.yml --limit web1 --check --diff是试运行。请阅读差异。ansible-playbook site.yml --limit web1 --diff将更改应用到这台主机。- 再次运行第 4 步。所有内容都应报告为
ok。仍报告changed的内容必须先修复,然后才能应用到其他主机。 - 现在对整个清单运行
ansible-playbook site.yml --check --diff,即可获得有意义的结果,因为已达到目标状态的主机不会产生输出,剩余内容才是真实差异。
关于第 3 步,有一点需要注意。--diff 会将文件内容输出到终端和 CI 任务日志中,因此,如果模板渲染数据库密码,该密码也会写入日志。请在该任务上设置 diff: false 以抑制输出,或设置 no_log: true 以隐藏整个结果,并将密码本身保存在 加密的 Ansible Vault 文件 中,而不是存放在代码库中。
FAQ
ansible-playbook --check 会修改服务器上的任何内容吗?
不会,但有一个由您控制的例外。在检查模式下,系统会要求每个模块报告结果,而不是执行写入;无法执行此操作的模块则不报告任何内容,也不执行任何操作。例外是 check_mode: false 任务关键字。它会强制单个任务实际执行,即使是在 --check 运行期间也是如此。在信任试运行结果前,请在 playbook 和角色中搜索 check_mode: false,并确认每个匹配项都只读取状态。
--check 和 --diff 有什么区别?
--check 决定是否实际执行操作。--diff 决定显示多少详细信息。单独使用 --check 时,它会告诉您某个文件将发生变化。单独使用 --diff 时,它会应用更改,并显示已修改的行。将两者一起使用,可以获得易于阅读的试运行结果。实际运行时也应启用 --diff,方法是在 ansible.cfg 的 [diff] 下设置 always = true。
为什么我的 Ansible 任务每次运行都报告 changed?
因为模块无法读取它管理的状态,或者您传入的值每次都不同。command 和 shell 始终报告 changed,除非您添加 creates 或 changed_when。使用 state: touch 的 file 会按设计产生变化。会渲染时间戳或新生成密码的模板每次都会生成不同的字节,因此文件确实会被重新写入。连续运行两次 playbook:第二次运行时仍为 changed 的任务就是需要修复的任务。
为什么我的 command 和 shell 任务在试运行期间会被跳过?
因为任意命令都没有只读执行方式。在检查模式下,command 模块会设置 skipped: true,并显示消息 Command would have run if not in check mode。请添加 creates 或 removes,以便检查模式改为评估文件测试。对于只读取状态的任务,请同时设置 check_mode: false 和 changed_when: false,这样注册结果在试运行期间仍会存在,基于该结果的条件也能继续生效。
为什么检查模式在新服务器上失败,但在已有服务器上成功?
因为检查模式不会创建后续任务所依赖的状态。对未安装 nginx 的主机执行试运行时,安装任务会报告为 changed,随后在写入 /etc/nginx/conf.d/ 的任务上失败,因为该目录从未创建。这是预期行为。检查模式用于检测 playbook 已经使主机达到目标状态后的偏差,无法验证首次运行。在新主机上,先将 playbook 应用到一台机器,然后查看第二次运行的结果。