Chapter 8: Roles & Reuse
Topic 48

include_role and import_role

RolesApplication

There are three ways to apply a role, and the difference between them is static versus dynamic — the same parse-time-versus-runtime split that separated import_tasks from include_tasks in Chapter 7. The roles: keyword runs roles before the play's tasks. import_role inserts a role statically at a chosen point in your task list. include_role applies one dynamically at runtime, which is what lets you loop a role or apply it conditionally on a value that does not exist until the play is running.

Pick the wrong one and the symptom is specific: a loop that runs once, a --list-tasks that comes up empty, a when that does not behave the way you expected. Each form has exactly one job it is best at, and the three cover every way you would want to apply larkspur_web.

The roles: Keyword

Listed at the play level, roles in the roles: block run before any tasks: in the play. This is the cleanest form when a host simply "is a web server" and the role applies unconditionally up front — no condition, no loop, just "apply larkspur_web to every host in this play." The role's tasks execute first, then the play's own tasks, in that fixed order. If a host's whole identity is the role, this is the form to reach for.

import_role — Static, Mid-play

import_role inserts the role's tasks inline at parse time, wherever you place it among other tasks. Because it resolves before the run, the role is visible to --list-tasks — every task inside shows up in the pre-run listing. Tags and when placed on the import apply to every task in the role, propagated down at parse time. Use it when a role needs to run at a specific point in the middle of a task list, not just before everything else.

import_role — static, placed mid-play, visible to --list-tasks
tasks:
  - name: Prepare the host
    ansible.builtin.import_tasks: prep.yml

  - name: Apply the web role here, after prep
    ansible.builtin.import_role:
      name: larkspur_web
    tags: [web]         # propagates to every task in the role

Run this with --list-tasks and every task inside larkspur_web appears, because the import was resolved before the run started. The web tag attaches to all of them, so --tags web selects the whole role's worth of tasks at once.

include_role — Dynamic, Runtime

include_role applies the role when execution reaches it. A loop over include_role runs the role once per item, and a when on it is evaluated live, against values that exist only at runtime. The cost is the same trade as include_tasks: imported-only features do not fully apply. The role is opaque to --list-tasks and --start-at-task, because there is nothing to list until the play actually reaches the include and resolves it.

Two ways to apply a role — the same static/dynamic split as tasks
import_role
Static, parse-time. Tasks resolve before the run, so --list-tasks sees them and a tag propagates to every one. Cannot loop or branch on a runtime value.
include_role
Dynamic, run-time. Resolves when execution reaches it, so it can loop and take a live when. Opaque to --list-tasks until the play gets there.

Passing Variables to a Role

vars: on the role application sets values for that one invocation — precedence above the role's defaults, scoped to the role. This is how you run larkspur_web once with larkspur_web_nginx_port: 443 and again with a different port, without touching the role's source. The value lives only for that application; it does not leak into later plays or other roles, which is exactly what you want when the same role serves two hosts with two configurations.

include_role in a loop — dynamic application, per-invocation vars
- name: Configure each vhost with the web role
  ansible.builtin.include_role:
    name: larkspur_web
  vars:
    larkspur_web_nginx_port: "{{ item.port }}"
    larkspur_web_app_path: "{{ item.path }}"
  loop: "{{ larkspur_vhosts }}"

This loops only because it is include_role — the role runs once per vhost, with that vhost's port and path scoped to each invocation. Swap in import_role and the loop breaks, because a static import is resolved once at parse time and has nothing to iterate over.

Looping a Role

Only include_role loops. import_role is static and cannot take a loop — applying larkspur_web across a list of vhosts is the canonical case where dynamic application is required, not a preference. If you need the same role applied N times with N different inputs, the dynamic form is the only one that does it. When the count and the conditions are fixed and known up front, prefer the static roles: or import_role and keep the role visible to your pre-run tooling.

import_role vs include_role

import_role — resolves statically at parse time, so tags and --list-tasks see every task inside it. It cannot loop or branch on a runtime value. Use it for an unconditional role placed at a fixed point among your tasks.

include_role — resolves at runtime, so it can loop and take a live when, but it is opaque to parse-time tooling like --list-tasks. Reach for it the moment you need to loop the role or decide whether to apply it on a value that only exists during the run.

Common Mistakes
  • Putting a loop on import_role and getting one execution or an error, because import is static and only include_role iterates over a list.
  • Expecting --list-tasks or --start-at-task to see inside an include_role — dynamic application is invisible to parse-time tooling, the same trade as include_tasks.
  • Applying a role with roles: when it needed to run after a specific task, then fighting the ordering because roles: always runs before tasks:.
  • Setting role variables via a global set_fact instead of vars: on the application, leaking the value into later plays and other roles instead of scoping it to one invocation.
  • Adding a when to import_role and assuming it skips the role cheaply — the condition is applied to every task individually at runtime, which works but is not the same as not importing the role at all.
Best Practices
  • Use the play-level roles: keyword for roles that apply unconditionally up front, and import_role to place a role at a specific point among other tasks.
  • Reach for include_role only when you need to loop a role or branch on a runtime value, accepting that it is opaque to --list-tasks.
  • Pass per-invocation values with vars: on the role application so the same role serves web1 and web2 with different settings and no source edits.
  • Tag at the import_role level when you want --tags to select a whole role's worth of tasks, since static import propagates the tag to every task inside it.
  • Default to a static form and switch to include_role deliberately, so you keep parse-time visibility unless a loop or live condition forces dynamic application.
Comparable tools Terraform module blocks with for_each — the loop-a-reusable-unit analog Puppet include vs resource-style class mirrors the static/dynamic split loosely Plain shell scripting — no clean equivalent for applying a named reusable unit

Knowledge Check

You need to apply larkspur_web once per vhost in a list. Which form must you use?

  • include_role with a loop — only dynamic application iterates over a list
  • import_role with a loop — static import is fine as long as the list is known at parse time
  • The roles: keyword with a loop attached at the play level
  • Any of the three — looping a role works identically across all forms

Why does an include_role not show up in --list-tasks when import_role does?

  • include_role resolves at runtime, so nothing exists to list until the play reaches it, whereas import_role resolves at parse time
  • include_role tasks are intentionally hidden from the listing for security reasons by default
  • --list-tasks only ever prints tasks that carry an explicit name attribute, and dynamic includes silently strip those task names away
  • Included roles run on the control node itself, which --list-tasks deliberately does not inspect

You pass vars: on a role application to set larkspur_web_nginx_port. What is its scope?

  • It applies to that single invocation of the role, above the role's defaults, and does not leak into later plays or other roles
  • It becomes a global host-wide fact for the rest of the run, exactly as a set_fact would
  • It permanently overwrites the stored value in the role's defaults/main.yml file on disk
  • It is silently ignored at runtime unless the very same variable is also declared up front inside the role's own vars/main.yml file

A role applied with the play-level roles: keyword needs to run after a specific setup task. What goes wrong?

  • Roles in roles: always run before any tasks: in the play, so it fires too early — use import_role placed after the task instead
  • The role already runs after the tasks: section by default, so nothing actually goes wrong here
  • Ansible silently reorders the play's sections at runtime to satisfy the implied setup-task ordering dependency for you automatically
  • The role refuses to run at all because a setup task happened to precede it in the same play

You got correct