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

Ansible 实战

把当初用 shell 凑合的事情做成插件

在 TT Lab 中继续学习

目标

亲手做出 Jinja2 filter、test、lookup 和自定义模块,并在 playbook 中调用。通过代码确认这四种分别在哪里运行,以及这种位置的差别让什么能做、让什么不能做。

为什么重要

playbook 一变大,哪个团队都会出现同样的位置——整理名称、给数字分类、到某处查一张表。如果标准 filter 做不到,人们就用 shell 和 sed 凑合,从那一刻起,那个 task 就失去了幂等性和检查模式。插件就是为这个位置准备的。不过不是随便放在哪里都行,在哪里运行是设计的一半。filter、test、lookup 在控制节点上运行,所以不需要在目标主机上安装任何东西,但看不到目标主机的状态;模块被复制到目标主机并在那里运行,所以可以改变状态,但必须自己负责幂等性和检查模式。本实验在这条边界上来回跨越四次,亲手确认。

步骤

  1. 创建 /root/ansplug/ansible.cfg,把默认 inventory 指定为 ./inventory/hosts.ini。在 /root/ansplug/inventory/hosts.ini 中写三个组——web 中有 web1(svc_port 8080,svc_name Web Front 01)和 web2(9090,Web Front 02),db 中有 db1(5432,Main DB!),edge 中有 cache1(443,Edge Cache)。四台主机都是 ansible_host=127.0.0.1 ansible_port=2222,[all:vars] 中的 ansible_user 是 root。然后运行 ansible all -m ansible.builtin.ping -o,把标准输出和标准错误一起保存到 /root/ansplug/out/ping.txt。
  2. 创建 /root/ansplug/filter_plugins/labfilters.py,注册一个 slugify filter。这个 filter 把字符串转为小写,把所有非字母数字的字符都换成 -,把连续出现的 - 合并成一个,并去掉两端的 -。如果传入的不是字符串,就抛出 AnsibleFilterError。然后创建 /root/ansplug/slug.yml,把 'Web Server 01!!' 交给这个 filter 得到的结果写到 /root/ansplug/out/slug.txt,并运行。
  3. 在同一个 /root/ansplug/filter_plugins/labfilters.py 中再加一个 port_class filter。port_class(value, privileged_below=1024, dynamic_from=49152) 在端口小于 privileged_below 时返回 system,大于等于 dynamic_from 时返回 dynamic,介于两者之间时返回 user,如果不是整数就抛出 AnsibleFilterError。然后创建 /root/ansplug/ports.yml,把 inventory 的四台主机按名称升序,每行一台以 <호스트> <포트> <분류>(占位符依次为主机、端口、分类)写到 /root/ansplug/out/ports.txt,并运行。
  4. 创建 /root/ansplug/test_plugins/labtests.py,注册 reserved_port test——端口号小于 1024 时为真。然后创建 /root/ansplug/reserved.yml,把四台主机按名称升序,每行一台以 <호스트> <포트> <reserved|free>(占位符依次为主机、端口、reserved 或 free)写到 /root/ansplug/out/reserved.txt,并运行。判定必须以 is reserved_port 的形式调用。
  5. 在 /root/ansplug/registry.json 中写入 {"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}。创建 /root/ansplug/lookup_plugins/labregistry.py,定义 labregistry lookup——接收一个键并返回注册表中的值,可以用 registry= 关键字参数指定其他文件(默认值是 /root/ansplug/registry.json),文件不存在或键不存在时抛出 AnsibleError。然后用 /root/ansplug/regions.yml 把 db、edge、web 三个组按这个顺序以 <그룹> <지역>(占位符依次为组、区域)写到 /root/ansplug/out/regions.txt,并运行。
  6. 在 /root/ansplug/collections/ansible_collections/labhub/site/ 中放置 collection——galaxy.yml 的 namespace 是 labhub,name 是 site,filter 文件放在 plugins/filter/ 下(把第 2、3 步做好的原样复制过来即可)。在 /root/ansplug/ansible.cfg 中加上 collections_path = ./collections,用 /root/ansplug/fqcn.yml 把 'Prod DB 02!!' 交给 labhub.site.slugify 得到的结果写到 /root/ansplug/out/fqcn.txt,并运行。
  7. 在 /root/ansplug/library/lab_marker.py 中创建自定义模块 lab_marker——参数是 path 和 content,如果文件内容已经相同就以 changed=false 结束,不同就写入文件并以 changed=true 结束。声明 supports_check_mode=True,并且在检查模式下不写入。用 /root/ansplug/marker.yml 对 web 组运行这个模块,让它向 /root/ansplug/out/<호스트이름>.marker(占位符为主机名)写入那台主机的名称一行,把 playbook 运行两次,把第二次运行的输出保存到 /root/ansplug/out/marker_run2.txt。
  8. 用 /root/ansplug/report.yml 把四台主机按名称升序每行一台写到 /root/ansplug/out/report.txt——格式是 <호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(占位符依次为主机、名称 slug、端口、端口分类、reserved 或 free、区域),名称 slug 是对 svc_name 做 slugify 的值,端口分类是 port_class,第五栏是 reserved_port test,区域是用 labregistry 查询该主机第一个组名得到的值。把 playbook 运行两次,并把第二次运行的输出保存到 /root/ansplug/out/report_run2.txt。

参考

先搭起模拟四台主机的 inventory

创建 /root/ansplug/ansible.cfg,把默认 inventory 指定为 ./inventory/hosts.ini。在 /root/ansplug/inventory/hosts.ini 中写三个组——web 中有 web1(svc_port 8080,svc_name Web Front 01)和 web2(9090,Web Front 02),db 中有 db1(5432,Main DB!),edge 中有 cache1(443,Edge Cache)。四台主机都是 ansible_host=127.0.0.1 ansible_port=2222,[all:vars] 中的 ansible_user 是 root。然后运行 ansible all -m ansible.builtin.ping -o,把标准输出和标准错误一起保存到 /root/ansplug/out/ping.txt。

这个 Pod 的 sshd 运行在 127.0.0.1 的 2222 端口上,并且可以密钥认证。在 inventory 中写多个名称,也全都连到同一个 sshd,所以可以模拟“多台”。在 INI inventory 中,值里有空格就用双引号括起来。合并后的结果用 ansible-inventory --list 确认。

第一个 filter——在控制节点上运行的纯函数

创建 /root/ansplug/filter_plugins/labfilters.py,注册一个 slugify filter。这个 filter 把字符串转为小写,把所有非字母数字的字符都换成 -,把连续出现的 - 合并成一个,并去掉两端的 -。如果传入的不是字符串,就抛出 AnsibleFilterError。然后创建 /root/ansplug/slug.yml,把 'Web Server 01!!' 交给这个 filter 得到的结果写到 /root/ansplug/out/slug.txt,并运行。

Ansible 会在 filter_plugins/ 内的 Python 文件中查找名为 FilterModule 的类,并把该类的 filters() 所返回字典的键用作 filter 名称。这个目录必须在 playbook 旁边才能被找到——在 ad-hoc 命令中找不到。filter 在控制节点上运行,所以不需要在目标主机上安装任何东西,也因此更要是没有副作用的纯函数。

接收参数的 filter,以及截断错误输入的位置

在同一个 /root/ansplug/filter_plugins/labfilters.py 中再加一个 port_class filter。port_class(value, privileged_below=1024, dynamic_from=49152) 在端口小于 privileged_below 时返回 system,大于等于 dynamic_from 时返回 dynamic,介于两者之间时返回 user,如果不是整数就抛出 AnsibleFilterError。然后创建 /root/ansplug/ports.yml,把 inventory 的四台主机按名称升序,每行一台以 <호스트> <포트> <분류>(占位符依次为主机、端口、分类)写到 /root/ansplug/out/ports.txt,并运行。

Jinja2 filter 把管道左边的值作为第一个参数,括号里写的是后面的参数——就像 {{ 3000 | port_class(2000, 4000) }} 这样。把默认值放在 Python 一侧,大多数调用不带参数就结束,只有特殊的地方才改变边界来调用。在 Python 中 True 是 int 的子类型,会通过 isinstance(x, int)——必须先把布尔值筛掉。遍历所有主机用的是 groups['all'] 和 hostvars[...]。

test 插件——只返回真假的位置

创建 /root/ansplug/test_plugins/labtests.py,注册 reserved_port test——端口号小于 1024 时为真。然后创建 /root/ansplug/reserved.yml,把四台主机按名称升序,每行一台以 <호스트> <포트> <reserved|free>(占位符依次为主机、端口、reserved 或 free)写到 /root/ansplug/out/reserved.txt,并运行。判定必须以 is reserved_port 的形式调用。

test 与 filter 的类名和方法名都不同——是 TestModule 和 tests()。目录也是 test_plugins/。调用方式也不同。filter 是 값 | 이름,test 是 값 is 이름(占位符依次为值、名称),而且 test 必须只返回真或假。之所以有这个区分,是因为 when: 和 select、reject 是以 test 为前提来使用的——返回不是真假的值,含义当场就会崩塌。

lookup——读取控制节点文件的位置

在 /root/ansplug/registry.json 中写入 {"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}。创建 /root/ansplug/lookup_plugins/labregistry.py,定义 labregistry lookup——接收一个键并返回注册表中的值,可以用 registry= 关键字参数指定其他文件(默认值是 /root/ansplug/registry.json),文件不存在或键不存在时抛出 AnsibleError。然后用 /root/ansplug/regions.yml 把 db、edge、web 三个组按这个顺序以 <그룹> <지역>(占位符依次为组、区域)写到 /root/ansplug/out/regions.txt,并运行。

lookup 是继承 LookupBase 的 LookupModule,入口是 run(self, terms, variables=None, **kwargs)。terms 是 lookup('이름', 첫째, 둘째)(占位符依次为名称、第一个、第二个)的参数列表,必须返回列表。通过 kwargs 传来的是 registry= 这样的关键字参数。重要的是这段代码在控制节点上运行——lookup 读取的文件必须在运行 playbook 的位置,而不是目标服务器上。

把同一个 filter 迁入 collection,并用 FQCN 调用

在 /root/ansplug/collections/ansible_collections/labhub/site/ 中放置 collection——galaxy.yml 的 namespace 是 labhub,name 是 site,filter 文件放在 plugins/filter/ 下(把第 2、3 步做好的原样复制过来即可)。在 /root/ansplug/ansible.cfg 中加上 collections_path = ./collections,用 /root/ansplug/fqcn.yml 把 'Prod DB 02!!' 交给 labhub.site.slugify 得到的结果写到 /root/ansplug/out/fqcn.txt,并运行。

collection 的位置固定为 <검색경로>/ansible_collections/<네임스페이스>/<이름>/(占位符依次为搜索路径、命名空间、名称)——这三层只要错开一层,就会不报任何错误地找不到。在 collection 内,filter 的位置不是 filter_plugins/ 而是 plugins/filter/。test 是 plugins/test/,lookup 是 plugins/lookup/,模块是 plugins/modules/。是否正确找到,用 ansible-config dump | grep COLLECTIONS 确认。

模块在目标主机上运行——幂等性和检查模式由它自己负责

在 /root/ansplug/library/lab_marker.py 中创建自定义模块 lab_marker——参数是 path 和 content,如果文件内容已经相同就以 changed=false 结束,不同就写入文件并以 changed=true 结束。声明 supports_check_mode=True,并且在检查模式下不写入。用 /root/ansplug/marker.yml 对 web 组运行这个模块,让它向 /root/ansplug/out/<호스트이름>.marker(占位符为主机名)写入那台主机的名称一行,把 playbook 运行两次,把第二次运行的输出保存到 /root/ansplug/out/marker_run2.txt。

与 filter、test、lookup 不同,模块会被复制到目标主机并在那里执行。所以读不到控制节点的文件,但可以改变状态——能改变意味着必须自己负责幂等性和检查模式。模块的骨架是用 AnsibleModule(argument_spec=..., supports_check_mode=True) 接收参数,用 module.exit_json(changed=...) 结束。如果漏掉 supports_check_mode,在 --check 中那个 task 会被直接跳过——不是失败而是沉默,所以更危险。模块在 playbook 旁边的 library/ 中查找。

在一个 playbook 中把四种串起来

用 /root/ansplug/report.yml 把四台主机按名称升序每行一台写到 /root/ansplug/out/report.txt——格式是 <호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(占位符依次为主机、名称 slug、端口、端口分类、reserved 或 free、区域),名称 slug 是对 svc_name 做 slugify 的值,端口分类是 port_class,第五栏是 reserved_port test,区域是用 labregistry 查询该主机第一个组名得到的值。把 playbook 运行两次,并把第二次运行的输出保存到 /root/ansplug/out/report_run2.txt。

一台主机所属的组名在 hostvars[h].group_names 里,这里它的第一个元素就是注册表的键。把 Jinja2 的 {% for %} 块原样放进 copy 模块的 content,可以一次生成多行。输入相同则文件内容相同,所以第二次运行必须是 changed=0——这就是“生成报告的 playbook”具备幂等性的证据。