Ansible 如何在一个 Playbook 中运行另一个 Playbook
Ansible 中 import_playbook、import_tasks 与 include_tasks 作用不同。本文通过实验对比三者差异,教你如何根据需求选择正确指令,并使用 ansible-playbook --list-tasks 命令验证执行顺序。
从另一个 Ansible playbook 运行 playbook 的三种方式
Ansible 提供了三种从一个 playbook 运行另一个 playbook 的方法,它们不可互换。import_playbook 将整个 playbook 文件(包括所有 play)拼接到父 playbook 中。import_tasks 在解析 playbook 时将任务文件拉入 play。include_tasks 在 play 运行期间将任务文件拉入 play。大多数搜索此功能的用户需要的是第一种:一个 site.yml,按顺序指定要运行的 playbook。
这两种任务机制的选择并非个人偏好问题。它决定了 ansible-playbook --list-tasks 可以看到哪些任务,以及 --tags 是否会深入读取文件内容。本指南将构建一个完全在当前机器上运行的小型实验环境,然后通过 --list-tasks 和 --list-tags 来展示其差异,而非仅仅进行理论说明。
设置一个无需第二台服务器即可运行的实验环境
从发行版软件包中安装 Ansible 并记录安装结果。
sudo apt update
sudo apt install -y ansible
ansible-playbook --versionansible-playbook --version 的第一行是 ansible-core 版本,其下方列出了它读取的配置文件以及将运行模块的 Python 解释器。在提交关于行为与本页描述不符的错误报告之前,请先记下该版本号。此处描述的 import 和 include 分离机制自 ansible-core 2.4 版本起已趋于稳定,因此截至 2026 年 9 月,任何当前发行版提供的版本其行为均保持一致。
现在创建一个工作目录,并创建一个指向本机的清单文件。
mkdir -p ~/ansible-lab/tasks
cd ~/ansible-lab
printf 'localhost ansible_connection=local\n' > inventory.iniansible_connection=local 使 Ansible 在本地以子进程方式运行每个任务,而不是向自身发起 SSH (secure shell) 连接。如果省略此项,连接会失败,导致任务无法执行。你可以跳过该文件,改在命令行中使用 -i 'localhost,' -c local,其中末尾的逗号用于告知 Ansible 将该参数解析为主机列表而非文件名。清单文件如何决定 Play 的目标主机 涵盖了该格式的其余部分,而 第一个 Playbook 演练 则涵盖了本指南假设你已掌握的 Play 相关内容。
site.yml:import_playbook 用于导入整个 playbook
Playbook 文件是由多个 play 组成的列表。import_playbook 是该列表中唯一允许存在的非 play 条目。编写方式如下 site.yml:
---
- import_playbook: provision.yml
- import_playbook: verify.yml这就是该功能的全部内容。Ansible 在解析 site.yml 时会同时读取这两个文件,并按照你编写的顺序将它们的 play 拼接在一起。每个被导入的 playbook 都会保留其自身的 hosts: 行,因此这两个文件可以针对不同的主机组。目前没有针对整个 playbook 的动态导入方式,因此 playbook 导入始终是静态的。
“在解析时读取”这一特性引申出两条规则。import_playbook 只能出现在 playbook 的顶层,绝不能出现在 play 的 tasks: 下方,因为任务列表不能包含 play 级别的关键字。此外,路径不能依赖于运行期间发现的值(例如 fact 或注册的变量结果),因为文件在连接任何主机之前就已经被打开了。
在 Play 中使用:import_tasks 与 include_tasks 的对比
provision.yml 是一个同时使用这两种任务机制的普通 Play,你可以将其放在同一个文件中进行对比:
---
- name: Provision the workspace
hosts: localhost
gather_facts: false
vars:
workspace: "{{ playbook_dir }}/build"
tasks:
- name: Create the workspace directory
ansible.builtin.file:
path: "{{ workspace }}"
state: directory
mode: "0755"
- ansible.builtin.import_tasks: tasks/write_files.yml
- name: Run the checks
ansible.builtin.include_tasks: tasks/checks.ymltasks/write_files.yml 包含两个任务,其中一个带有标签:
---
- name: Write the marker file
ansible.builtin.copy:
content: "workspace ready\n"
dest: "{{ workspace }}/marker.txt"
mode: "0644"
- name: Write the app config
ansible.builtin.copy:
content: |
[app]
name = demo
dest: "{{ workspace }}/app.ini"
mode: "0644"
tags:
- configtasks/checks.yml 包含另外两个任务,其中一个带有不同的标签:
---
- name: Read the marker file back
ansible.builtin.command: cat {{ workspace }}/marker.txt
register: marker
changed_when: false
- name: Show what the marker file holds
ansible.builtin.debug:
var: marker.stdout
tags:
- reportchanged_when: false 阻止 Ansible 将读取操作报告为变更,因为 command 模块无法判断 cat 是否未进行任何更改。
verify.yml 是第二个被导入的 Playbook,它是一个独立的 Play:
---
- name: Verify the workspace
hosts: localhost
gather_facts: false
vars:
workspace: "{{ playbook_dir }}/build"
tasks:
- name: Confirm the app config exists
ansible.builtin.command: test -f {{ workspace }}/app.ini
changed_when: false
- name: Report the workspace path
ansible.builtin.debug:
msg: "Workspace verified at {{ workspace }}"运行整个流程。
ansible-playbook -i inventory.ini site.yml你应该会看到两个 Play 按顺序执行,即 Provision the workspace 和 Verify the workspace,并在最后看到包含 ok 计数且没有 failed 的汇总信息。如果第二个 Play 在 test -f 任务上失败,说明文件从未被写入,此时应查看第一个 Play 的输出而非第二个。文件缺失会导致 test 以非零状态码退出,这正是验证任务应有的表现。
--list-tasks 有什么作用?
--list-tasks 会解析 playbook 并打印出它发现的任务。它不会连接任何主机,也不会进行任何更改,因此在生产环境文件上运行是安全的。
ansible-playbook -i inventory.ini --list-tasks site.yml对照你编写的文件查看输出。两个导入的 playbook 都显示为带编号的 play,Provision the workspace 为第一个,Verify the workspace 为第二个。这证明了 import_playbook 是在解析阶段完成解析的。在第一个 play 下,你会看到 Create the workspace directory,随后是 Write the marker file 和 Write the app config,它们以各自的名称列出,尽管这些名称在 provision.yml 中并未出现。导入的内容已经被扁平化合并到了 play 中。
现在观察缺失的内容。Read the marker file back 和 Show what the marker file holds 不在列表中。tasks/checks.yml 唯一的痕迹是一行 Run the checks,即 include_tasks 任务本身的名称。Ansible 无法列出其内部内容,因为在解析阶段它尚未打开该文件。它只包含一个任务,其唯一工作是在稍后打开该文件。
--list-tags 的作用是什么?
同理,深入一层分析。
ansible-playbook -i inventory.ini --list-tags site.yml第一个 play 的标签列表包含 config,因为该标签位于一个静态导入的任务上,而该任务现在已成为 play 的一部分。report 不在列表中,因为它位于一个尚未被读取的文件内。这不仅是一个显示问题:
ansible-playbook -i inventory.ini --tags report site.yml该运行不会产生任何实际效果。--tags report 会保留标记为 report 的任务并跳过其余所有任务。Run the checks 任务本身没有标签,因此它被跳过,导致文件从未被打开,其中的带标签任务也就没有机会进行匹配。解决方法是为 include 任务本身添加标签,并将标签下推到其包含的内容中:
- name: Run the checks
ansible.builtin.include_tasks:
file: tasks/checks.yml
apply:
tags:
- report
tags:
- report重新运行 --list-tags,report 依然不会出现,因为其内容在解析阶段仍不可见。重新运行 --tags report,检查任务现在可以执行,因为 include 任务本身匹配了过滤器。
解析时与运行时的对比及其代价
上述所有差异均源于一个根本区别。import 在 playbook 被读取时解析,因此在运行开始时,import 已不存在,只剩下任务。include 本身是一个任务,它在 play 执行到该行时发挥作用。
静态 import 带来了可见性。在运行开始前,任务即已确定,因此 --list-tasks 可以显示它们,--tags 可以逐一匹配它们,而写在 import_tasks 行上的 when: 会被复制到每个导入的任务中并分别评估。其代价是所有内容必须在解析时可知:不能使用 loop:,文件名也不能由事实(fact)构建。
动态 include 带来了运行时的灵活性。文件名可以来自变量(包括由先前任务设置的变量),且 loop: 可以正常工作,文件会针对每个条目重新读取。写在 include_tasks 行上的 when: 只会被评估一次,因此结果为 false 时会跳过整个文件,而不是跳过文件内的每个任务。其代价是牺牲了可见性:审查工具、--list-tasks 和 --tags 都会在 include 行处停止。
除非需要动态形式才能实现的功能,否则请使用 import_tasks。审查者阅读 --list-tasks 输出时,看到的是运行的实际计划,而文件中的每个 include 都是该计划中的盲点。
此处还需说明一个作用域细节。Handler 的作用域是 play 级别的,因此在 provision.yml 内部通知的 handler 会在整个 play 结束时运行,而不是在 verify.yml 完成后立即运行。因此,将一个长 playbook 拆分为多个导入的 playbook 会改变重启发生的时机。模板变更如何通知 handler 一文详细介绍了该路径。
变量作用域的陷阱
请注意,verify.yml 重复了与 provision.yml 相同的 vars: 块。这是必需的,并非疏忽。Play 中的 vars: 块仅属于该 Play。导入的 Playbook 会引入其各自独立的 Play,因此第二个 Play 不会继承第一个 Play 的任何变量。如果从 verify.yml 中删除 vars: 块,运行将在引用 workspace 的第一个任务处停止,并报错提示变量未定义。
根本的解决方法是在 site.yml 旁边使用 group_vars/all.yml。导入的 Playbook 中的每个 Play 都会通过 Inventory 读取这些值,因此只需编写一次,两个 Play 都能读取到。你也可以在导入语句本身传递这些值:
- import_playbook: verify.yml
vars:
workspace: /home/deploy/ansible-lab/build对于仅在运行开始后才存在的值,请使用 set_fact。在主机上设置的 Fact 会在该主机的整个运行期间保持有效,因此后续针对同一主机的 Play 可以读取该值。
您究竟需要哪种机制?
如果您要运行的对象拥有自己的 hosts: 行,则它是一个 playbook,请在 site.yml 中使用 import_playbook。如果您要运行的是在 play 中重复使用的任务块,且您在解析时已确定需要运行它,请使用 import_tasks。如果待运行的文件取决于运行时发现的内容,或者您需要针对列表中的每个项目运行一次,请使用 include_tasks。
如果任务文件本身包含变量和处理程序,则它应该是一个 role,Ansible 会自动加载其目录结构,无需任何导入行。Playbook 与 role 的区别 涵盖了该界限的划分,因此本指南不再赘述。
行之有效的结构:site.yml 与按用途划分的 playbook
在实际运维规模中,能够经受考验的布局是:在根目录下放置一个 site.yml,其中仅包含导入指令,并在其下方为每个用途分别建立一个 playbook。
site.yml
inventory.ini
group_vars/all.yml
provision.yml
verify.yml
tasks/write_files.yml
tasks/checks.ymlsite.yml 可以在十秒内读完,且当你只需要执行某一部分时,每个子 playbook 仍可独立运行。在正式执行前,请务必先以检查模式运行整个流程。
ansible-playbook -i inventory.ini --check --diff site.yml检查模式会报告将要发生的变更,但不会实际执行,而 --diff 会打印出即将写入的内容。本实验中有一个注意事项:command 和 shell 任务在检查模式下默认会被跳过,因为 Ansible 无法判断任意命令是否可以安全执行。因此,上述两个 command 任务会显示为已跳过,此处的检查运行将侧重于审查你的 file 和 copy 任务,而不是模拟整个过程。检查模式的实际测试范围 详细说明了在检查模式下报告不准确的模块,而 在集群中运行单个 playbook 则涵盖了当 site.yml 的目标不再仅限于 localhost 时的清单配置。
请将 --list-tasks 纳入你的审查习惯。在重构前后分别运行它,两次输出之间的差异即为该计划的纯文本差异对比。这是审查 playbook 拆分最快的方法,也是唯一能以 Ansible 相同方式读取文件的方法。
FAQ
为什么 --tags 会跳过我包含的文件中的任务?
因为 include_tasks 文件中的任务标签只有在文件被读取后才存在,而只有当 include 任务本身运行时才会读取该文件。使用 --tags report 时,Ansible 会跳过所有不带 report 的任务,这其中也包括未打标签的 include 任务,因此该文件永远不会被打开。请为 include_tasks 任务本身打上标签,并添加 apply: tags: 块,这样内部的任务也会继承该标签。使用 import_tasks 不会出现此问题,因为这些任务在标签过滤开始前就已经属于 play 的一部分了。
我可以在 import_playbook 的路径中使用变量吗?
只能使用在 playbook 解析时已经有值的变量,例如通过命令行 -e 传递的变量或在 group_vars 中定义的变量。运行过程中发现的任何变量都为时已晚,因为 import_playbook 在连接第一个主机之前就会打开文件,所以事实(fact)、注册结果或 set_fact 值不能出现在该路径中。整个 playbook 没有动态 include,因此当必须在运行时选择文件时,请将该选择移至 play 内部的 include_tasks 中。
为什么第一个 playbook 中的变量在第二个中显示未定义?
因为 play 上的 vars: 仅限于该 play 的作用域,而导入的 playbook 会贡献其各自独立的 play。第二个 play 启动时没有这些变量,第一个引用该变量的任务会因未定义变量错误而失败,并指出该变量名。请将共享值移至 group_vars/all.yml,以便每个 play 都能从清单中读取它们,或者在 import_playbook 行的 vars: 块中传递它们。对于运行期间发现的值,请使用 set_fact,因为在主机上设置的事实会在运行的剩余时间内保留在该主机上。
默认情况下我应该使用 import_tasks 还是 include_tasks?
请使用 import_tasks。它在解析时被解析,因此其任务会出现在 ansible-playbook --list-tasks 中,--tags 可以逐个匹配它们,任何阅读列表的人都能看到运行将遵循的计划。仅在 include_tasks 独有的功能场景下才切换使用它:例如从运行产生的值中选择文件,或使用 loop: 为每个项目运行同一个文件。你添加的每一个 include 都是 --list-tasks 无法再为你展示的运行片段。