读懂 Ansible 剧本
你可以把一个 Ansible Playbook(.yml 文件)想象成一个剧本。这篇笔记将带你从零开始,彻底理解一个剧本的构成,让你能看懂并写出自己的自动化任务。
- Playbook (剧本): 整个文件,定义了要完成的一系列任务。
- Play (戏剧/任务集): 剧本由一个或多个
Play组成。每个 Play 都是一个顶级YAML列表项(以-开头)。它定义了 **“谁”(目标主机)**来 “做什么”(一系列任务)。 - Task (任务): 每个 Play 包含一系列
Task。这些是具体的 “动作”,比如执行一个命令、安装一个软件。 - Module (模块): 每个 Task 都通过调用一个 Ansible
Module来执行。shell、file、set_fact都是模块。
一、 Play 的关键指令:定义“谁”和“环境”
Play 是你的 YAML 文件中的顶级 - 元素,它定义了任务执行的上下文。一个典型的 Playbook 采用“采集-汇总”模式,包含两个 Play:第一个在所有目标机上采集信息,第二个在本地汇总。
# Play 1: 在所有目标主机上执行
- hosts: all
become: yes
become_user: root
gather_facts: true
tasks:
- ...
# Play 2: 仅在本地执行
- hosts: localhost
gather_facts: false
vars:
counts: 0
tasks:
- ...
-
hosts: 定义目标主机。这是 Play 中最重要的部分。all: 表示 Ansible 主机清单(inventory)中的所有主机。localhost: 表示仅在运行 Ansible 命令的本机上执行。- 也可以是主机组名(如
webservers)、具体 IP 或域名。
-
become/become_user: 提权设置。become: yes: 相当于sudo,告诉 Ansible 需要提升权限来执行任务。become_user: root: 指定要切换到的用户,通常是root。
-
gather_facts: 收集“事实”。true: 在执行 tasks 之前,Ansible 会自动连接到目标主机,收集关于该主机的各种信息(如操作系统ansible_distribution、主机名ansible_hostname等)。这些信息被称为 "Facts",可在后续任务中当做变量使用。false: 跳过此步骤以提速,但ansible_开头的内置变量将无法使用。
-
vars: 定义 Play 级别的变量。在这里定义的变量,可以在当前 Play 的所有tasks中使用,通常用于设置初始值。
二、 Task 的关键指令:定义“做什么”
Tasks 是 Play 的核心,定义了具体要执行的操作。
tasks:
- name: 僵尸进程检测
shell: ps -A -o stat,pid | grep -e ^[zZ]|awk {print $2}
register: PID
changed_when: false
ignore_errors: true
when: ansible_distribution == "Rocky"
-
name: 任务的可读描述。它会显示在 Ansible 的执行输出中,强烈建议为每个任务都写一个清晰的name。 -
模块名: 每个任务必须调用一个模块,如shell,set_fact,file,debug。 -
register: 注册变量。这是 Ansible 中极其重要的指令。它会将一个任务的执行结果(包括标准输出、错误、返回码等)保存到一个你指定的变量中。- 例如,
register: PID后,你就可以通过PID.stdout(标准输出字符串) 或PID.stdout_lines(标准输出按行分割的列表) 来使用其结果。
- 例如,
-
when: 条件判断。只有当when后面的表达式为真(True)时,这个任务才会执行。 -
block: 任务块。可以将多个任务组织成一个逻辑单元,常与when结合,实现根据不同条件执行不同的一组任务,使逻辑非常清晰。 -
with_items: 循环。可以对一个列表进行迭代,对每个列表项都执行一次该任务。循环中,当前项的值会赋给item变量。 -
changed_when: 自定义“变更”状态。很多查询类命令并不实际改变系统状态,使用changed_when: false可以防止 Ansible 将其错误地标记为"changed"。 -
ignore_errors: true: 忽略错误。即使任务失败,Ansible 也会继续执行后续任务,而不是中断整个流程。
三、 Ansible 的魔法:变量、事实与 Jinja2
所有 {{ ... }} 语法都属于 Jinja2 模板引擎,它让 Playbook 变得动态和强大。
-
变量引用:
{{ a_variable }}{{ ansible_hostname }}: 引用gather_facts收集到的主机名。{{ PID.stdout_lines }}: 引用register注册的PID变量的stdout_lines属性。
-
过滤器 (Filter): 使用
|管道符对变量进行处理,功能强大。| length: 获取列表的长度或字符串的字符数。| replace('\n', '<br/>'): 将字符串中的换行符\n替换为 HTML 的<br/>。| int: 将变量的值转换成整数类型。| to_json: 将复杂的变量(如字典或列表)转换成 JSON 格式的字符串。
-
set_fact: 主动创建一个新变量(“事实”)。这个变量在当前主机的后续任务中都可以使用,非常适合用来整理和格式化数据。 -
hostvars: 跨主机变量访问。这是 Ansible 一个非常强大的特性,是“采集-汇总”模式的核心。-
它是一个特殊的、巨大的变量,包含了清单中所有主机的所有变量信息。
-
用法:
hostvars[主机名][变量名]。 -
应用: 在
localhost的 Play 中,汇总来自所有其他主机的数据。- name: 整合结果
set_fact:
contents: "{{ contents + hostvars[item].content }}"
counts: "{{ counts|int + hostvars[item].count|int }}"
with_items: "{{ groups.all }}"这里的
item就是通过with_items遍历得到的主机名。通过hostvars[item].content就可以精确地获取该item主机在第一个 Play 中通过set_fact设置的content变量的值。
-
四、深度解析:“列表累加”的秘密
在上面的例子中,counts 的累加是数字相加,很好理解。但 contents 的累加是什么意思?
对于列表(List/Array)来说,+ 号操作符不是数字相加,而是 列表拼接 (List Concatenation)。它将两个列表合并成一个新列表。
我们来分解一下这个拼接过程,假设有两台服务器 server1 和 server2:
-
初始状态 在
localhost上,contents变量初始值为:contents:
- hostname: ""
result: ""
# ... 其他空字段 -
第一次循环 (
item= server1)- Ansible 从
server1获取到hostvars[server1].content的值,假设是:[{hostname: "server1", result: "无超多200个僵尸进程", ...}]。 - 执行拼接操作:
[{hostname: "", ...}] + [{hostname: "server1", ...}] contents变量被更新为包含两个元素的列表。
- Ansible 从
-
第二次循环 (
item= server2)- Ansible 从
server2获取到hostvars[server2].content的值,假设是:[{hostname: "server2", result: "有超过200个僵尸进程请处理", ...}]。 - 执行拼接操作:
[{hostname: "", ...}, {hostname: "server1", ...}] + [{hostname: "server2", ...}] contents变量被更新为包含三个元素的列表。
- Ansible 从
循环结束后,localhost 上的 contents 变量就包含了一个由所有主机的巡检结果组成的、完整的列表,为最终生成报告做好了准备。