unit 6 · Making it real
import vs include
Parse time or runtime — pick on purpose
you may want to read 24 · Roles: structure, defaults vs vars first — but nothing is locked
tl;dr
- import_tasks/import_role are static: resolved at parse time. include_tasks/include_role: at runtime.
- Default to import for parse-time checking; use include for variable filenames, loops, per-host paths.
- Bare include: was removed in ansible-core 2.16 — playbooks still using it fail to parse.
why it matters Every role and split-out task file enters a play through one of these keywords; the wrong pick breaks variable filenames and loops, or throws away the parse-time checks that catch typos early.
Lesson 24 left your work split across files — role task files, plus any long play you break into pieces. Two keyword families pull those files back in, and they differ on one axis: when Ansible resolves them. Everything in this lesson falls out of that fact.
import_tasks and import_role are static. Ansible resolves them at
parse time — while reading your playbook, before contacting a single
host. The imported tasks are inlined into the play, as if you’d pasted
them in by hand.
include_tasks and include_role are dynamic. Ansible resolves them
at runtime, when execution reaches that task. By then facts and per-host
variables exist and can decide what gets pulled in.
- name: Baseline packages # static — inlined at parse time
ansible.builtin.import_tasks: baseline.yml
- name: OS-specific setup # dynamic — file name resolved per host, at runtime
ansible.builtin.include_tasks: '{{ ansible_os_family }}.yml'What static buys you
Because imports are inlined while parsing, the entire play is known before
the run starts. A typo’d module name or broken YAML inside an imported file
fails immediately — not twenty minutes into a change window. --list-tasks
shows the imported tasks by name, and --start-at-task can jump straight to
one (both return in lesson 29). Boring, checkable, predictable — exactly
what you want by default.
What dynamic buys you
An include is just a task until it runs. That’s the point: at runtime Ansible knows things the parser can’t — which host it’s on, what facts came back, what a loop variable currently holds. Only includes can do these:
- Variable file names.
'{{ ansible_os_family }}.yml'picks a different file per host. Parse time has no per-host anything, so import can’t. - Loops.
loop:on an import is a hard error; oninclude_tasksorinclude_roleit processes the file once per item. - Skip the file entirely. A false condition on an include means the file is never even opened.
- name: Apply one hardening role per item
ansible.builtin.include_role:
name: '{{ item }}' # runtime value — import_role could never resolve this
loop:
- firewall_baseline
- audit_policyThe bill for that flexibility: the parser can’t see inside. --list-tasks
shows the include line and nothing beneath it. A typo inside the included
file only surfaces when execution arrives.
The decision rule is short. Import by default; include when you need runtime behavior. A literal file name, no loop, no per-host branching — import it and take the free checking. The moment a variable, loop, or per-host decision enters the picture, switch to include.
The ancestor: bare include:
Before these two families existed, one keyword tried to be both at once:
# old repos and pre-2.16 tutorials are full of this
- include: legacy-tasks.ymlIts half-static, half-dynamic behavior was ambiguous, so it was
deprecated in ansible-core 2.12 and
removed in 2.16 . On a modern control node,
a playbook containing it fails to parse. Like bare module names in
lesson 4, it dates a repo at a glance. Replace it on sight:
import_tasks by default, include_tasks if the call site needs runtime
behavior.
One habit ties this lesson together: on an unfamiliar play, run
--list-tasks first. What it shows is imported; what it hides is
included — that gap isn’t a bug, it’s the design.
self-check — recall, not a test
1 A play has include_tasks: "{{ ansible_os_family }}.yml". Could you swap in import_tasks? Why or why not?
2 --list-tasks shows the tasks from one of your task files but not the other. What does that tell you?
3 You find a bare include: in an old repo. What happens on a modern control node, and what do you replace it with?