Skip to content

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

site.yaml
- 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
setup.yaml
- name: Setup
  ansible.builtin.debug:
    msg: I am Setup
configure.yaml
- name: Configure
  ansible.builtin.debug:
    msg: I am Configure
verify.yaml
- name: Verify
  ansible.builtin.debug:
    msg: I am Verify

Each 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-tasks
playbook: 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_tasks for structure that’s fixed and always applies the same way, and take the --list-tasks visibility as a genuine benefit, not an afterthought.
  • Use include_tasks specifically when you need runtime flexibility — a variable-determined filename, or repetition via loop:.
  • 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_tasks for fixed, always-applicable structure, and include_tasks the moment runtime data — a variable, a loop — needs to influence what gets included or how many times.
  • Revisit any past use of include_tasks you 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.
Last updated on