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

Ansible 实战

规则、配置档、例外,以及把它们串起来的关卡

在 TT Lab 中继续学习

一句话总结

--syntax-check 只检查 YAML 是否讲得通,质量由 ansible-lint 来看。而让 lint 长期存活的诀窍不是开启很多规则,而是把例外放在尽可能小的范围内。

为什么需要它

Ansible 之所以危险,是因为写错的 playbook 也能以绿色结束。没有名称的 task、带管道的 shell、没有指定权限的文件写入,全都会以 ok 或 changed 结束。问题在六个月后出现。

lint 把这六个月提前到提交之前。但一开启,团队马上会遇到下一个问题。对旧仓库运行 lint 会冒出几百条指摘,其中有几条确实需要例外。这时例外怎么放,决定了 lint 半年后是否还活着。

工作原理

层次不同

工具 看什么 看不到什么
--syntax-check YAML 结构、play 和 task 的形状、模块名称是否真实存在 质量、幂等性、值是否合理
ansible-lint 名称、幂等性、权限、FQCN 这类规范 运行时的值
assert task 值是否合理(端口范围、环境名称) 静态规范
molecule 真正启动 role,做收敛、幂等性、验证场景 不能替代上面三者

越往下越贵。所以关卡要先运行便宜的。

规则 id 与 profile

ansible-lint 的指摘总会附上规则 id。也有像 name[play] 这样用方括号区分细项的。下面是本课程实验镜像中的版本(6.17.2)实际会出现的规则。

规则 id 抓什么
name[play] 没有名称的 play
name[missing] 没有名称的 task
name[casing] 名称以小写字母开头
no-free-form 像 copy: src=a dest=b 这样写成一行的调用
no-changed-when 可能改变状态的命令没有报告标准
risky-shell-pipe 使用管道却没有设置 pipefail
risky-file-permissions 创建文件时没有指定 mode
command-instead-of-module 有专用模块的命令却用 shell 调用
fqcn[action-core] 短模块名称

规则被 profile 捆绑在一起。按 min、basic、moderate、safety、shared、production 的顺序,越往上越严格,上面的 profile 包含下面所有的规则。所以把 lint 引入旧仓库时,不会一步到 production。先用 basic 让 CI 变绿,下个月再升到 moderate。profile 存在的理由就是这条迁移路径。

版本不同,规则名称也不同。所以一定要先用 ansible-lint -L 查看自己版本的列表。

放置例外的两个位置

- name: Pack the release bundle  # noqa: command-instead-of-module
  ansible.builtin.command: tar -czf /tmp/rel.tgz -C /srv app.conf
  changed_when: false

# noqa: <규칙id>(占位符为规则 id)只把那个 task 从该规则中排除。它就留在旁边,所以评审时可以问“为什么排除”,理由消失后也可以删除。

# .ansible-lint
profile: production
exclude_paths:
  - legacy/
skip_list:
  - name[casing]

skip_list 会在整个仓库中关掉该规则。只用于团队达成共识的规范(例如:允许以产品名称小写字母开头的 task 名称)。如果因为指摘太多就把规则一股脑塞进这里,剩下的就只有“lint 是开着的”这一错觉。

exclude_paths 又不同。它不是关掉规则,而是完全不扫描那个路径。放进去的是别人写的代码,或为了教学而保留的坏例子。不过,如果直接把文件名作为参数给出,这个列表就会被忽略——排除是“扫描时”的规则。

设置文件存在的真正理由不是方便,而是让人手和 CI 用同一套规则运行。如果只在 CI 中给出 --profile production,开发者以为通过了就推送,然后在 CI 里失败。

lint 看不到的位置——assert

lint 看的是静态规范。但事故也会出在值上。端口里传进来 80,环境名称有错别字,副本数变成 0。这些只有在运行时才能知道,所以用 ansible.builtin.assert 让 playbook 自己去问。

- name: Assert that the port is usable
  ansible.builtin.assert:
    that:
      - app_port is integer
      - app_port >= 1024
    fail_msg: "app_port must be an integer of 1024 or above, got {{ app_port }}"

有两点重要。第一,要在改变任何东西之前就问。如果部署了一半才停下,回滚的代价要高得多。第二,一定要写 fail_msg。没有它,失败消息会以条件式原文的形式出现,收到的人不知道该改什么。另外,用 -e 传入的值如果不另外指定,就是字符串——is integer 为什么为假,就在这里分晓。

molecule 还能多做什么

Molecule 是测试 role 的框架。按场景启动目标(Docker、Podman、云),应用 role,再应用一次看是不是 changed=0(幂等性),用验证 playbook 确认结果,然后清理掉。它能自动看到 lint 看不到的“是否真的收敛”。

本实验镜像中没有 molecule。 实验 Pod 没有互联网,无法安装,启动容器也被禁止。所以本模块的实验去掉 molecule,只搭建它的前一段(语法、lint、前置条件)。即使在使用 molecule 的团队,这前一段也照样需要——因为 molecule 要花几分钟,而语法检查连 1 秒都不到。

在现场相遇的样子

第一,引入总是从“不要更糟”开始,而不是“全部修好”。 用较低的 profile 让 CI 变绿,只对新代码应用高标准,旧路径先放进 exclude_paths,动到的时候再取出来。

第二,no-changed-when 指摘的一半是真正的缺陷。 给查询命令加上 changed_when: false,不是在凑格式,而是让报告变得诚实。永远是 changed 的 playbook,会把真正有东西发生变化的那一天藏起来。

第三,关卡脚本一定要确认到它真的能拦住。 只检查、却永远以 0 结束的脚本其实很常见。这样的关卡比没有还糟——因为它制造了“正在检查”的错觉。做出来的当天就故意喂给它坏输入,看它是否以非 0 值结束,是那个脚本的第一项测试。

第四,例外是有期限的。 加 # noqa 时,如果在同一行或正上方用一行写明为什么加,半年后理由消失时就可以删掉。没有写明理由的例外会永远留着。

参考文档

下一项实验要做什么

故意写一个能通过语法检查的坏 playbook,把 lint 抓到的规则 id 提取出来留成列表。把做同样事情的干净 playbook 提升到 basic profile,再加上 FQCN 和 mode 提升到 production。对一个没有对应模块的命令,用 # noqa 只在一行上设置例外,并在 .ansible-lint 中写明 profile、排除路径和要跳过的规则,让整个仓库一次通过。用 assert 拦住前置条件,最后做一个一次性检查语法、lint 和前置条件的关卡脚本,并确认喂给它坏目录时它真的会拦住。