Chapter 7: Managing Complex Plays and Playbooks
Including and importing files
A playbook that does everything in one file is easy to start and hard to live with. Past a few dozen tasks it is slow to read, risky to change, and impossible to reuse. Ansible lets you split work across files at two levels: whole playbooks, and lists of tasks.
- Combine playbooks with
import_playbook. - Move tasks into task files and reuse them with
import_tasksandinclude_tasks. - Explain the difference between a static import and a dynamic include, and choose between them.
- Pass variables to a task file so one file serves several plays.
Two levels of reuse
A playbook can pull in other complete playbooks. The result runs as if all their plays were written one after another in a single file.
A play can pull in a file that holds nothing but a list of tasks. The same file can serve many plays.
Select a file to see what is in it and how the pieces connect:
One project, five small files
Importing playbooks
import_playbook is written at the top level of a playbook, where a play would go. It cannot be used inside a play.
---
- name: Web tier
ansible.builtin.import_playbook: web.yml
- name: Database tier
ansible.builtin.import_playbook: db.ymlYou can mix imported playbooks and ordinary plays in one file; everything runs in the order it is listed.
- name: Prepare the control node
hosts: localhost
tasks:
- ansible.builtin.debug:
msg: Starting the roll-out
- name: Web tier
ansible.builtin.import_playbook: web.ymlEach imported file stays a normal playbook that you can also run by itself, which is what makes the split worthwhile: web.yml for a quick change to the web servers, site.yml for everything.
An imported playbook can take vars: as well. The values apply to every play in that file, which lets one generic playbook serve several purposes:
- name: Database tier
ansible.builtin.import_playbook: install.yml
vars:
package: mariadb-serverTask files
A task file is a flat YAML list of tasks. It has no hosts: and no tasks: keyword, because it is not a play. Keep them together in a tasks/ directory next to the playbook, so the structure of a large project stays easy to see:
---
- name: Packages are installed
ansible.builtin.dnf:
name: "{{ packages }}"
state: presentA play brings it in with one of two keywords. Both are used like a module, as a task:
tasks:
- name: Packages
ansible.builtin.import_tasks: tasks/install.yml
- name: Packages
ansible.builtin.include_tasks: tasks/install.ymlThe path is relative to the file that contains the task. Keeping task files in a tasks/ directory next to the playbook is a convention worth following.
Static or dynamic
The two keywords differ in when Ansible reads the file.
Read while the playbook is being parsed, before anything runs. The tasks are copied into the play, as if you had pasted them there.
Read when the play reaches that task, separately for each host. Until then Ansible does not know what is inside.
That one difference explains everything else. Switch the keyword, then add a condition or a loop:
The consequences side by side:
import_tasks | include_tasks | |
|---|---|---|
| File is read | When the playbook is parsed | When the task runs |
when on it | Copied to every task in the file; each tests it | Tested once; if false the file is never read |
loop on it | Not allowed | Runs the file once per item |
| File name from a variable | Only variables known at parse time (play vars, -e) | Any variable, including facts and registered results |
--list-tasks and --start-at-task | See the tasks inside | See only the include itself |
| A syntax error in the file | Found before the run starts | Found when that task is reached |
Passing variables
A task file is most useful when it does not hard-code what it works on. Give it variables with vars: on the import or include:
- name: Web servers are configured
hosts: web
tasks:
- name: Packages
ansible.builtin.import_tasks: tasks/install.yml
vars:
packages:
- httpd
- firewalld- name: Database servers are configured
hosts: db
tasks:
- name: Packages
ansible.builtin.import_tasks: tasks/install.yml
vars:
packages:
- mariadb-serverOne file, two plays, two different package lists. Variables set this way exist only for the tasks of that import.
When an include runs in a loop, each pass sees the current item:
- name: Firewall services
ansible.builtin.include_tasks: tasks/firewall.yml
loop:
- http
- httpsTASK [Firewall services] *******************************************************
included: /home/student/project/tasks/firewall.yml for servera.lab.example.com => (item=http)
included: /home/student/project/tasks/firewall.yml for servera.lab.example.com => (item=https)Where task files pay off
- Building a new server in stages: users, packages, services, sudo rules, shared file systems, hardening, updates, monitoring. Each stage is its own file, easy to find and to test.
- Several teams, one playbook. Developers, system administrators and database administrators each own a task file; one playbook brings them together.
- Optional configuration, included only when a condition says a host needs it.
- Group-specific work: a task file imported in a play that targets only that group.
A file name that depends on the host
Because an include is resolved at run time, its file name can come from a fact:
- name: Distribution-specific setup
ansible.builtin.include_tasks: "tasks/setup-{{ ansible_facts['os_family'] }}.yml"The same line with import_tasks fails before the play starts, because facts are not gathered yet when the playbook is parsed:
ERROR! Error when evaluating variable in import path: tasks/{{ ansible_facts['os_family'] }}.yml.The same limit applies to host and group variables from the inventory: they belong to individual hosts, and an import is resolved once for the whole play. A variable in an import path can come from the play, a variables file or the command line (-e), but not from the inventory.
- Where can `import_playbook` appear?
- What does a task file contain?
- You need to run a task file once for each item in a list. Which keyword?
- A play has `import_tasks: tasks/web.yml` with `when: deploy_web`, and `deploy_web` is false. What appears in the output?
- Which keyword lets `ansible-navigator run --list-tasks` show the tasks inside the file?
Takeaway
Combine playbooks with import_playbook. Use the chapter lab to check this on a real host.