合并后的清单 — 谁赢了,又该怎么核实
一句话总结
inventory 来源有两个以上时,主机和组会被合并,同名变量则是后读取的获胜。决定这个“后”的,是 -i 的书写顺序、目录内的文件名顺序,以及组的深度和字典序。
为什么需要它
inventory 只有一个文件时,读一读就明白了。但只靠一个文件的组织很少。公共主机由平台团队管理,各服务的主机由各团队管理。生产和预发布把文件分开,避免误混。云资源不靠人来写,而是由动态 inventory 插件生成。这样一来,-i 会出现两次以上,到了某个时刻,就再没有人能靠读文件回答“现在 web1 的 app_port 是多少”了。
由此产生的事故有两种。第一种是混入了不该有的服务器。两个文件中如果有同名的组,主机就会被合并,所以原以为只在预发布文件里的主机,进入了生产部署的对象。第二种是不该获胜的值获胜了。旧文件中残留的组变量覆盖了新文件的值,或者相反,新建的 group_vars/ 文件悄悄挤掉了 inventory 内的值。
这两种事故,都无法用“读文件来推断”的方式来防止。防止的方法只有一个——在运行之前,向机器询问合并后的结果。
工作原理
多次给出 -i 时
可以像 ansible-playbook -i a.ini -i b.ini site.yml 这样多次给出。来源会按书写顺序读取,结果合并成一个 inventory。
| 什么 | 合并时 |
|---|---|
| 主机 | 并集。同名就是同一台主机 |
| 组 | 并集。同名则成员合并 |
| 同名变量 | 后读取的来源获胜 |
同名的主机出现在两个文件中时,不会变成两台,而是合并成一台。即使 ansible_host 写得不同也一样——后面的获胜,前面的值会毫无警告地消失。在迁移 inventory 时,如果新增了一个主机名不变、只改了地址的文件,而又不知道这个行为,就会折腾好几天。
给出目录时
如果像 -i inventory/ 这样给出目录,会按名称顺序读取其中的文件。所以惯例是像 10-base、20-prod、30-overrides 这样加数字前缀。名称决定顺序,顺序决定胜负。
这里有一个一定会坑到初学者一次的陷阱。目录 inventory 默认会跳过某些扩展名。配置项名称是 inventory_ignore_extensions,它的默认列表中包含 .ini。.cfg、.retry、.md、.txt,以及编辑器的备份文件(.bak、以波浪号结尾的文件)也是如此。
所以 inventory/hosts.ini,用 -i inventory/hosts.ini 可以读取,而用 -i inventory/ 就读取不到。不会有任何错误。只是相当于那个文件里写的主机不存在。放在目录里的 INI 格式 inventory,要去掉扩展名(10-base),或者使用别的名称。YAML inventory 的 .yml、.yaml 不在列表中,所以会正常读取。
磁盘上的 group_vars 与 host_vars
写 inventory 变量的地方有两处。一处是 inventory 文件内部([web:vars] 或 YAML 的 vars:),另一处是 inventory 旁边的 group_vars/、host_vars/ 目录。如果两处定义了同一个变量,谁会获胜?
inventory/
10-base [web:vars] app_port=8080
group_vars/
all.yml app_port: 9000 owner: platform
web.yml app_port: 9090
host_vars/
web1.yml app_port: 9999
结果是这样。web1 是 9999,web2 是 9090,其他组的主机是 9000。写在 inventory 文件内部的 8080 哪里都不会出现。因为在官方文档的优先级列表中,“inventory file or script group vars”位于“inventory group_vars/*”的下面。即使在同一个目录中,文件内部的组变量也是最弱的——这是实际工作中最常让人吃惊的地方。
如果用一行来背顺序,就是这样。inventory 文件内部的组变量 → group_vars/all → group_vars/所属组 → inventory 文件内部的主机变量 → host_vars/主机。越往后越窄,而窄的获胜。
group_vars/ 目录可以放在 inventory 旁边和 playbook 旁边两处,同名时 playbook 旁边的获胜。如果分放在两处,半年后就没有人能找到值,所以最好定为一处,并写成团队规则。
相同深度的组重叠时
如果一台主机属于多个组,而这些组定义了同一个变量,会怎样?默认规则是按字典序合并,后面的获胜。同时属于 alpha 和 zulu 的主机,会得到 zulu 的值。字典序不可能包含什么含义,所以依赖这个默认值的设计本身就是危险信号。
能把默认值颠倒过来的旋钮是 ansible_group_priority。默认值是 1,数字越大,越晚合并,也就越获胜。
[alpha:vars]
color=from-alpha
ansible_group_priority=10
[zulu:vars]
color=from-zulu
这样设置之后,alpha 会压过字典序靠后的 zulu 而获胜。有一点要注意——这个变量写在 inventory 来源内部才会生效。写在 group_vars/alpha.yml 中则不会生效。因为它是用来决定读入那些文件的顺序的值,写在那些文件里就已经晚了。
而且这个优先级只在相同深度的组之间才有意义。父组和子组之间,是深度获胜。
子组胜过父组
如果把 web 和 db 放在 [prod:children] 之下,web 就是 prod 的子组。两个组定义了同一个变量时,作为子组的 web 获胜。与字典序无关——即使父组名为 zprod,字典序靠后,子组也获胜。
这条规则很自然。父组宽、子组窄,始终是窄的获胜。在 prod 中设置公共默认值,只在 web 中覆盖需要的部分,这种设计因此才成立。all 是所有组的祖先,所以最宽,也最弱。
询问合并后结果的方法
怀疑 inventory 时,盯着文件看是浪费时间。答案就在三个命令里。
| 命令 | 回答什么 |
|---|---|
ansible-inventory -i ... --graph |
组结构和成员。哪台主机在哪个组 |
ansible-inventory -i ... --list |
合并后的全部内容,以 JSON 输出。适合用脚本检查 |
ansible-inventory -i ... --host web1 |
该主机最终拥有的全部变量 |
--host 尤其在查看值时使用。它能在 3 秒内回答“web1 的 app_port 到底是多少”。给出 --graph --vars,图中还会一并显示变量。如果在 CI 中设置一个检查这个命令结果的步骤,就可以在部署之前抓住“混入了不该有的服务器”这种事故。
主机模式——缩小对象的语法
inventory 变大之后,就需要“生产的 Web 服务器中不属于预发布的那些”这样的对象。模式语法就是做这件事的。
| 符号 | 含义 | 例 |
|---|---|---|
: |
并集 | web:db |
:& |
交集 | web:&prod |
:! |
差集 | prod:!staging |
* |
glob | web*.example.com |
~ |
正则表达式(加在前面) | 用正则表达式选择名称 |
把三个连起来,就是 web:&prod:!staging。这是属于 web、同时属于 prod、但不属于 staging 的主机。运行之前一定要用 --list-hosts 确认。尤其是差集,即使减完之后为空也不是错误,所以会出现没有任何对象、却以绿灯结束的部署。--limit 也使用相同的语法,会把 playbook 的 hosts: 中所写的模式再缩小一次。
动态 inventory 在这幅图的哪里
在云上,主机列表不是由人来写的。像 aws_ec2、gcp_compute 这样的 inventory 插件会查询 API,生成主机和组。合并的规则完全一样——动态来源也只是 -i 中所写的一个来源,按顺序与静态文件混合。常见的设计之所以是“主机列表来自动态来源,团队定的值来自静态的 group_vars/”,原因就在这里。如果动态来源只生成组名,与该名称对应的 group_vars/ 文件就会附上值。(这个实验环境中既没有互联网,也没有凭据,所以无法运行动态插件。只要记住规则是一样的就行。)
在现场相遇的样子
案例 1——目录把一个文件整个吞掉的那天。把 inventory 迁移到目录时,把原有的 hosts.ini 原样放进了 inventory/。运行 ansible-inventory --graph 一看,组只出来一半。没有任何错误。原因是 .ini 扩展名在默认忽略列表中,把文件名的扩展名去掉,当场就解决了。从那天起,有了一条规则:每一个修改 inventory 的 PR,都要附上 --graph 输出的差异。
案例 2——创建了 group_vars,值却没变的事。也有相反方向的事故。为了紧急改端口,修改了 inventory 文件内部的 [web:vars],值却没变。几个月前有人创建了 group_vars/web.yml,那一边更强。就是在那时学到,文件内部的组变量是最弱的位置。现在 inventory 文件里完全不写变量,只使用 group_vars/ 这一处。减少位置,比背优先级更好。
案例 3——依赖字典序的配置。app 和 backend 两个组里有同一台主机,两者都定义了 java_opts。按字典序,backend 一直获胜,有一天进行了把组名从 app 改为 zapp 的整理工作。只改了名称,获胜者就颠倒了,堆内存设置也整个变了。在这样的地方,要么用 ansible_group_priority 明确意图,要么一开始就修改设计,使一台主机不同时属于定义同一个变量的两个组。
下一项实验要做什么
从创建两个 inventory 文件、并给出两次 -i 开始。再把同样的内容放进目录,确认文件名顺序决定胜负,同时放一个 .ini 扩展名的文件,亲眼看到它被悄悄跳过这一事实。然后创建 group_vars/all、group_vars/그룹、host_vars/호스트(占位符依次为组名与主机名),测量每台主机上不同的值获胜的情况,并确认 inventory 文件内部的变量比这三者都弱。对于相同深度的两个组,尝试用 ansible_group_priority 颠倒字典序,并看到父组与子组之间是深度获胜。最后用模式缩小对象,把结果保存为文件,并亲手创建一个回答“这个变量到底哪个值获胜”的工具。