变量优先级与 facts
一句话总结
在 Ansible 中,所谓‘变量不生效’,多数不是因为变量不存在,而是因为在优先级更高的位置定义的值胜出了。
为什么需要它
假设要把同一个应用部署到 dev、stage、prod,只有端口、副本数和日志级别不同。如果为此复制三份 playbook,六个月后三个文件就会变成完全不同的东西。把值从代码中抽离并非风格偏好,而是防止分支不断增加的结构性选择。
问题在于,可以放置值的位置太多了:play 中的 vars、单独的 vars_files、group_vars/、host_vars/、inventory 中的变量、命令行 -e、role 的 defaults 与 vars,以及运行中通过 set_fact 创建的值。每个位置的优先级不同;不了解这个顺序,就会为‘明明改了值却没有生效’浪费一整天。
它如何工作
详细顺序很长,但实务中需要记住的骨架很短。
| 强度 | 位置 | 特性 |
|---|---|---|
| 最弱 | role 的 defaults/main.yml |
‘该 role 的默认值’——专为被覆盖而设 |
| 弱 | inventory 组变量 | 各环境的公共值 |
| 中等 | inventory 主机变量 | 仅该服务器使用的例外 |
| 强 | play 的 vars、vars_files |
本次运行使用的值 |
| 很强 | 通过 set_fact 创建的值 |
运行中计算出的值 |
| 最强 | 命令行 -e (extra vars) |
胜过所有其他值 |
只要遵守两项原则,大多数问题都能解决。**第一,可被覆盖的值放在 defaults 中。第二,-e 只用于一次性例外。**如果习惯性使用 -e,这个值不会记录在任何地方,之后的人便无法复现。
事实(fact)的性质不同。它不是人手填写的值,而是从目标服务器收集到的事实。当 gather_facts: true 时,play 开始时会运行 setup 模块,收集操作系统类型、架构、主机名、网络接口、内存等信息,并放入 ansible_facts。借助事实,可以针对不同目标自动执行‘Ubuntu 用 apt,RHEL 用 dnf’之类的分支。
收集事实是有成本的。主机达到数百台时,仅事实收集就可能耗费数十秒。不需要事实的 play 应以 gather_facts: false 关闭收集;若频繁重复运行,则应考虑事实缓存。
register 是第三种类型。它把任务的完整执行结果(标准输出、退出码、是否 changed)存入变量。常见错误是把整个结果对象原样写入文件,而实际需要的通常只是 .stdout。
实际工作中常见的情况
**第一,用 -e 救急后却不把修改落实到代码中。**如果在故障应对时传入 -e replicas=10 恢复了服务,这个值必须提交到仓库。否则下次部署会悄无声息地把它恢复原状。这和 Terraform 中的漂移事故在结构上完全相同。
第二,信任事实,但也要验证。ansible_distribution 等事实通常准确,但在容器或特殊镜像中可能出现意料之外的值。按事实分支时,应为意外值保留默认分支。
如何确认值来自哪里
比背诵优先级更实用的方法,是直接询问该主机此刻的值是什么。推断可能出错,实测不会。
最快的方法是用一个任务打印值。以 ad-hoc 方式运行 debug 输出指定变量,立刻就能看出以该主机为准的最终胜出值。尤其值得做的是一次对多台主机运行。若只有一台的值不同,说明该主机的 host_vars 或 inventory 行中写了例外,这通常就是问题根源。
如果关心的不是值,而是它来自哪里,就逐步缩小范围。暂时删除组变量后值发生变化,说明来源就在这里;若不变,则来自优先级更高的位置。运行时开启详细输出,日志会显示读取了哪些变量文件,从而能立即发现自己一直在修改一个根本未被读取的文件。
命名也会极大影响这个问题。养成为变量名添加前缀的习惯,几乎可以消除冲突。与 port 相比,myapp_port 更好,role 的变量应以 role 名作为前缀。短名称可能被任何 role 使用,一旦另一个 role 使用同名变量,其中一个就会悄无声息地败下阵来。
最后,比背诵优先级更有效的是,把什么内容放在哪个位置制定为团队规则。各环境不同的值放在组变量中,单台服务器的例外放在主机变量中,role 建议的默认值放在 defaults 中。只要遵守这三条,几乎无需使用其他位置;不用,就不会再被优先级吓到。
下一项实验要做什么
分别创建 play 变量、变量文件、组变量和主机变量,亲眼确认哪个会胜出,再观察命令行 -e 如何胜过所有这些值。收集事实并保存为 JSON,再用 register 和 set_fact 组合值,生成最终摘要文件。