include_role and import_role
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.
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.
--list-tasks sees them and a tag propagates to every one. Cannot loop or branch on a runtime value.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.
- 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 — 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.
- Putting a
looponimport_roleand getting one execution or an error, because import is static and onlyinclude_roleiterates over a list. - Expecting
--list-tasksor--start-at-taskto see inside aninclude_role— dynamic application is invisible to parse-time tooling, the same trade asinclude_tasks. - Applying a role with
roles:when it needed to run after a specific task, then fighting the ordering becauseroles:always runs beforetasks:. - Setting role variables via a global
set_factinstead ofvars:on the application, leaking the value into later plays and other roles instead of scoping it to one invocation. - Adding a
whentoimport_roleand 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.
- Use the play-level
roles:keyword for roles that apply unconditionally up front, andimport_roleto place a role at a specific point among other tasks. - Reach for
include_roleonly 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 servesweb1andweb2with different settings and no source edits. - Tag at the
import_rolelevel when you want--tagsto 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_roledeliberately, so you keep parse-time visibility unless a loop or live condition forces dynamic application.
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_rolewith aloop— only dynamic application iterates over a listimport_rolewith aloop— static import is fine as long as the list is known at parse time- The
roles:keyword with aloopattached 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_roleresolves at runtime, so nothing exists to list until the play reaches it, whereasimport_roleresolves at parse timeinclude_roletasks are intentionally hidden from the listing for security reasons by default--list-tasksonly ever prints tasks that carry an explicitnameattribute, and dynamic includes silently strip those task names away- Included roles run on the control node itself, which
--list-tasksdeliberately 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_factwould - It permanently overwrites the stored value in the role's
defaults/main.ymlfile 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.ymlfile
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 anytasks:in the play, so it fires too early — useimport_roleplaced 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