Skip to main content
RunBook Academy

AnsibleXXII · Roles and ReuseRoles and reuse

Static and dynamic role reuse

Intermediate⏱ ~22 minansible-playbook

What you'll learn

  • State which forms are resolved at parse time and which at run time
  • Predict what --list-tasks shows for each form, and why the difference matters
  • Choose between import and include from the operational consequence rather than habit
  • Recognise a dry run that under-reports what a play will do

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

There are four ways to pull other content into a play, and they divide into two pairs:

Static — parse timeDynamic — run time
roles: keyword
ansible.builtin.import_roleansible.builtin.include_role
ansible.builtin.import_tasksansible.builtin.include_tasks

Everything else follows from that one distinction. Static forms are expanded when the playbook is parsed, before a host is contacted. Dynamic forms are a task that runs, and what it pulls in does not exist until it does.

The consequences are operational rather than stylistic, and they are the sort of thing people repeat from memory incorrectly. Every claim below was executed against ansible-core 2.21.3.

What the parser knows

Two roles, alpha and beta. alpha has two tasks, one of them tagged alpha_inner. beta has one. A play imports the first and includes the second, tagging both statements patch:

- name: static versus dynamic
  hosts: web
  gather_facts: false
  tasks:
    - name: static import_role
      ansible.builtin.import_role:
        name: alpha
      tags: [patch]

    - name: dynamic include_role
      ansible.builtin.include_role:
        name: beta
      tags: [patch]
Read-only / Safeask the parser what it assembled
ansible-playbook -i inventory.ini listtasks.yml --list-tasks
Read-only / Safethe answer, executed on 2.21.3
$ ansible-playbook -i inventory.ini listtasks.yml --list-tasks
playbook: listtasks.yml

play #1 (web): static versus dynamic	TAGS: []
  tasks:
    alpha : alpha task one	TAGS: [alpha_inner, patch]
    alpha : alpha task two	TAGS: [patch]
    dynamic include_role	TAGS: [patch]

Read that carefully, because four separate facts are visible in five lines:

  1. The imported role is expanded. Both of alpha’s tasks appear, with the role-name prefix.
  2. Tags on the import statement propagate to every task inside it. Both alpha tasks carry patch, although neither declares it.
  3. A task’s own tags are additive. alpha task one has both alpha_inner and patch.
  4. The included role is one line. beta’s task does not appear at all. The parser does not know what is inside beta, because it has not looked.

The same split applies at task-file level. import_tasks expands; include_tasks does not:

Read-only / Safeimport_tasks versus include_tasks, same file included both ways
$ ansible-playbook -i inventory.ini incimp.yml --list-tasks
playbook: incimp.yml

play #1 (local): local	TAGS: []
  tasks:
    shared task one	TAGS: [inner, staticpart]
    shared task two	TAGS: [staticpart]
    dynamic include_tasks	TAGS: [dynpart]
    dynamic include_tasks in a loop	TAGS: []

Why that costs you something

--list-tasks is the cheapest pre-flight check there is. It costs no connection, no time and no risk, and it answers what is this run going to do. A play built from dynamic includes answers that question with a list of include statements.

The same blindness shows up in --list-tags, and there it is worse, because a tag you rely on can be missing from the list entirely:

Read-only / Safea play whose only content is a dynamic include
$ ansible-playbook -i inventory.ini dynonly.yml --list-tags
playbook: dynonly.yml

play #1 (local): local	TAGS: []
    TASK TAGS: []

An operator who runs --list-tags to find out what selections exist, sees nothing, and concludes the play has no tag structure, is reading an accurate report of what the parser knows and a false report of what the play contains.

What each form can and cannot do

The static forms trade flexibility for visibility, and the trade is enforced rather than advisory. Attempting to loop a static form is a hard parse error with a message that names the fix:

Read-only / Safeloop on import_tasks
$ ansible-playbook -i inventory.ini importloop.yml --syntax-check
[ERROR]: You cannot use loops on 'import_tasks' statements. You should use 'include_tasks' instead.
Origin: /srv/automation/importloop.yml:4:7

2   gather_facts: false
3   tasks:
4     - name: import_tasks with a loop
      ^ column 7
Read-only / Safeloop on import_role
$ ansible-playbook -i inventory.ini importroleloop.yml
[ERROR]: You cannot use loops on 'import_role' statements. You should use 'include_role' instead.
Origin: /srv/automation/importroleloop.yml:4:7

2   gather_facts: false
3   tasks:
4     - name: import_role with a loop
      ^ column 7

Summarised:

import_role / import_tasksinclude_role / include_tasks
ResolvedParse timeRun time
Appears in --list-tasksExpanded, task by taskOne line for the statement
Tags on the statementApplied to every task insideApplied to the statement only
Inner tags visible to --list-tagsYesNo
loop on the statementParse errorSupported
when on the statementCopied onto every inner task, evaluated per taskEvaluated once, for the include
Filename can be templated from a run-time factNoYes

The when row deserves a sentence. On a static import, when is not evaluated once for the block — it is copied onto each imported task and evaluated individually. Usually indistinguishable, but not always: if an early task in the imported file changes the thing the condition tests, later tasks in the same import can evaluate it differently.

Choosing

The decision is nearly always the same one:

Default to static. roles:, import_role, import_tasks. You get a complete dry run, working --tags, and a --list-tasks you can diff between branches to prove a refactor changed nothing.

Use dynamic when you need what only it does. Three genuine cases:

  1. A loop. Applying a role once per virtual host, per database, per environment.
  2. A path built from run-time information. The per-platform task file selected by ansible_facts['os_family'] cannot be resolved before facts are gathered.
  3. A conditional whose answer is expensive or unknowable at parse time, where you want to skip loading the content at all rather than skip its tasks individually.

Everything else — “it feels cleaner”, “the file is big” — is not a reason, and it costs you the dry run.

The dynamic form’s own conveniences

When you do use include_role, two options are worth knowing.

tasks_from: selects an entry point other than main.yml, which is how a role exposes more than one operation:

Read-only / Safetasks_from and vars on an include
$ ansible-playbook -i inventory.ini tasksfrom.yml
TASK [include only the verify entry point] *************************************

TASK [multi : multi verify] ****************************************************
ok: [localhost] => {
  "msg": "verify entry"
}

TASK [include with role vars] **************************************************

TASK [multi : multi main] ******************************************************
ok: [localhost] => {
  "msg": "main entry, greeting=passed-in"
}
- name: include only the verify entry point
  ansible.builtin.include_role:
    name: multi
    tasks_from: verify

- name: include with role vars
  ansible.builtin.include_role:
    name: multi
  vars:
    multi_greeting: passed-in

tasks_from pairs with the entry points in meta/argument_specs.yml from lesson 4: a role with tasks/verify.yml should declare a verify: entry point so its inputs are validated too.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play contains only ansible.builtin.include_tasks pointing at a file whose tasks carry a tag. What does --list-tags report for that play?

  2. Q2. Which are true of tags placed on an import_role statement? Select all that apply.

  3. Q3. Putting a loop on an import_role statement is a parse error, and the error message names include_role as the alternative.

  4. Q4. Which of these is a legitimate reason to choose include_role over import_role for production work?

Passing score: 75%. Answers are checked in this browser.