Organizing Includes and Imports
This closing chapter is about organization — splitting a large playbook into logical, maintainable pieces — and about closing a loop this course has left open twice already: exactly why two earlier examples specifically needed include_tasks, and genuinely couldn’t have used import_tasks instead.
Pattern: Splitting A Large Playbook Into Logical Files
- name: Full Deployment
hosts: servers
tasks:
- name: Run setup steps
ansible.builtin.import_tasks: setup.yaml
- name: Run configuration steps
ansible.builtin.import_tasks: configure.yaml
- name: Run verification steps
ansible.builtin.import_tasks: verify.yaml- name: Setup
ansible.builtin.debug:
msg: I am Setup- name: Configure
ansible.builtin.debug:
msg: I am Configure- name: Verify
ansible.builtin.debug:
msg: I am VerifyEach file holds a focused, reviewable chunk of related work — setup.yaml, configure.yaml, verify.yaml — rather than one long, undifferentiated task list. import_tasks is the right choice here specifically because none of this structure depends on runtime data; it’s always the same, every run, and you get full --list-tasks visibility into the entire thing as a bonus.
ansible-playbook site.yaml --list-tasksplaybook: site.yaml
play #1 (servers): Full Deployment TAGS: []
tasks:
Setup TAGS: []
Configure TAGS: []
Verify TAGS: []One thing to notice is that tasks’ name on site.yaml gets replaced by whatever the task is named on imported files. Therefore, it’s not necessary to name on site.yaml — commenting out the purpose could be good discipline here.
Revisiting The Nested-Loop Example: Why It Had To Be include_tasks
Back in the loops section, fixing the nested-loop naming collision required include_tasks, not import_tasks — and now the reason can be stated precisely:
- name: "Process items"
ansible.builtin.include_tasks: process_team.yaml
loop: "{{ teams }}"This only works because include_tasks is resolved fresh, at runtime, every time execution actually reaches it — and inside a loop:, execution reaches that line once per iteration. Each pass through the loop causes process_team.yaml to be read and run anew.
import_tasks is expanded exactly once, at parse time, before any loop could even begin iterating — there’s no “per iteration” for it to repeat against, because by the time the loop exists as a runtime concept, an import_tasks in that position would have already been fully spliced in just once. The nested-loop pattern isn’t just conventionally written with include_tasks — it’s structurally impossible with import_tasks.
Revisiting The Block-Looping Workaround
The same reasoning explains the block:-can’t-be-looped workaround from the blocks and handlers section. Moving a block’s tasks into a separate file and looping the file’s inclusion needed include_tasks for exactly the same reason: the tasks genuinely have to run fresh, once per loop item, and only a runtime-resolved include can do that.
General File-Organization Guidance
- Group related tasks into their own files by concern, not by arbitrary size —
database.yaml,webserver.yaml,setup.yaml,verify.yaml— whatever divisions make sense for the actual work being done. - Use
import_tasksfor structure that’s fixed and always applies the same way, and take the--list-tasksvisibility as a genuine benefit, not an afterthought. - Use
include_tasksspecifically when you need runtime flexibility — a variable-determined filename, or repetition vialoop:. - Keep a clear top-level entry point (commonly
site.yaml) that composes the pieces together, rather than one enormous flat playbook file.
What You Just Did
This section covered the single distinction underlying everything in it: parse-time versus runtime resolution. That one idea explained why a runtime-dependent filename breaks import_tasks but works fine with include_tasks; why there’s no dynamic equivalent for combining whole playbooks; why when: behaves completely differently depending on which form it’s attached to, and why --list-tasks can or can’t see inside an include; and now, precisely why two of this course’s own earlier examples were built the way they were, rather than just being told “use this one” without explanation.
Best Practices
- Default to
import_tasksfor fixed, always-applicable structure, andinclude_tasksthe moment runtime data — a variable, a loop — needs to influence what gets included or how many times. - Revisit any past use of
include_tasksyou didn’t fully understand at the time — chances are, like this course’s own nested-loop and block-looping examples, it was structurally necessary, not a stylistic choice. - Organize large playbooks around genuine concerns, composed through a single clear entry point, rather than either one giant file or an arbitrary scattering of small ones.