skip to content

In an Ansible playbook, what is the difference between import_role and include_role, and how does the choice affect tags, loops and when?

level: middleimportance: should knowfreq 58%

answer

  1. parse time versus run time
  2. one is spliced in, one is a task
  3. loops need the dynamic one
  4. tags cannot see inside a dynamic include
  5. when is copied onto every imported task

basics

~20 s

import_role is static: Ansible expands the role when it parses the playbook, so its tasks inherit tags and show up in --list-tasks, but it cannot be looped. include_role is dynamic: resolved at runtime, so it can be looped and skipped as one unit.

solid answer

~60 s

Everything follows from *when* the role is resolved. `ansible.builtin.import_role` is pre-processed at parse time: the role's tasks are spliced into the play before anything runs, so they appear in `--list-tasks`, a tag on the import is inherited by every child task, and a `when` on the import is copied onto each of those tasks and evaluated per task. The price is that nothing about the import can depend on runtime data - no `loop`, and no variable in the role name that is only known once the play starts. `ansible.builtin.include_role` is resolved at runtime: it is a single task, so it can be looped to apply a role several times, and a false `when` skips the whole role in one decision. The price is visibility - `--list-tasks` cannot see inside it, and `--tags` cannot select a tag that only exists on tasks within the role, so the include is skipped entirely unless the include itself carries the tag or you use the `apply` keyword to push tags onto its children.

code

yaml · 19 lines
yaml
- hosts: web
  tasks:
    # Static: expanded at parse time, tags reach every task inside
    - name: Apply the base role
      ansible.builtin.import_role:
        name: nginx
      tags: [nginx]

    # Dynamic: one runtime task, so it can loop and pick an entry point
    - name: Configure one vhost per site
      ansible.builtin.include_role:
        name: nginx
        tasks_from: vhost.yml
        apply:
          tags: [vhost]
      loop: "{{ sites }}"
      loop_control:
        loop_var: vhost
      tags: [vhost]

go deeper

for a junior

Know that import_role is static and include_role is dynamic, and that only include_role can be looped. Saying 'imports are resolved when the playbook is parsed' is enough at this level.

for a middle

Explain the consequences one by one: tag inheritance, --list-tasks visibility, per-task versus one-shot when, and why a runtime variable in the role name forces the dynamic form.

for a senior

Diagnose the real failures - a --tags run that silently does nothing because the include was untagged, a review that cannot see the tasks. Know the apply keyword and when a loop over include_role is the right shape.

for a principal

Set the house rule: import by default so pipelines can select by tag and review by --list-tasks, dynamic only where runtime data genuinely decides, and make that rule enforceable rather than folklore.

## One difference, many consequences Ansible has two families of task inclusion: **static imports** (`import_role`, `import_tasks`, `import_playbook`) and **dynamic includes** (`include_role`, `include_tasks`). The single underlying difference is timing. Imports are resolved when the playbook is parsed, before the first task executes. Includes are resolved when control reaches them, as ordinary tasks. Every behavioural difference below is downstream of that. ## What import_role gives you Because the role's tasks are flattened into the play at parse time, they become indistinguishable from tasks you wrote inline: - `ansible-playbook --list-tasks` shows every task in the role. - A tag placed on the `import_role` task is **inherited** by every task inside the role, so `--tags nginx` selects them. - A `when` on the import is **copied onto each child task**, so it is evaluated once per task, per host, at runtime. This surprises people: the condition is not evaluated once at import time, and if it references a fact, it is re-checked at each task. - Handlers the role defines are available immediately, including to tasks written before the import. The cost is that the import must be fully determined before the play runs. You cannot loop over it, and the role name cannot depend on a runtime variable such as a fact or a registered result. (Variables that are known at parse time - extra vars, inventory vars - do work in the name, but relying on that is fragile enough that most teams avoid it.) ## What include_role gives you An include is one task that happens to run a role, which buys flexibility: - It can carry `loop`, so a single statement applies a role once per item. - Its `when` is evaluated once, on the include itself; false means the entire role is skipped without evaluating anything inside. - The role name can come from a variable resolved at runtime. - Options such as `tasks_from`, `vars_from`, `defaults_from` and `handlers_from` let you enter the role at a non-default file. - `public: true` exposes the role's vars and defaults to the rest of the play afterwards; by default an included role's variables stay scoped to the include. The cost is opacity. Ansible does not know what is inside a dynamic include until it runs it, so: ```bash # Prints the include statement, not the role's tasks ansible-playbook site.yml --list-tasks # Skips the include entirely if the tag exists only INSIDE the role ansible-playbook site.yml --tags certs ``` Tag selection happens before the include is expanded, so a tag written on a task inside the role is invisible to `--tags`. The include must itself be tagged. If you also want that tag on the role's inner tasks, use the `apply` keyword: ```yaml - name: Renew certificates for each site ansible.builtin.include_role: name: certbot apply: tags: [certs] loop: "{{ sites }}" tags: [certs] ``` (The tag on the include itself is still required - `apply` propagates keywords inward, it does not make the include selectable.) ## The `roles:` keyword is a third thing Roles listed under a play's `roles:` header behave like static imports, and they run **before** the play's `tasks:` section. That ordering catches people out: a task written above `roles:` in the file still runs after the roles. Using `import_role`/`include_role` inside `tasks:` puts the role exactly where you wrote it, which is why explicit task-level application has largely displaced the `roles:` header in newer code. ## How to choose Default to `import_role` for the ordinary case of "apply this role here": you keep tag selectivity, `--list-tasks` output stays honest, and there is no runtime resolution to reason about. Reach for `include_role` when you genuinely need runtime behaviour - looping a role over a list, choosing the role by variable, entering at an alternate `tasks_from`, or skipping a large role with one cheap decision instead of tagging hundreds of copied conditionals onto its tasks. ## Failure modes to name in an interview A CI job that runs `--tags migrate` and silently does nothing, because the migration tasks live inside an untagged `include_role`. A `--check` or `--list-tasks` review that looks empty for the same reason. A `when: ansible_facts['os_family'] == 'Debian'` on an `import_role` that produces a wall of skipped tasks in the output rather than one skip, because the condition was copied onto every task. And an attempt to add `loop:` to `import_role`, which is not supported - the fix is to switch to `include_role`.

  • You run the playbook with --tags migrate and nothing happens, although tasks inside a role are tagged migrate. Why?
    The role is pulled in with `include_role`. Tag selection happens before a dynamic include is expanded, so Ansible cannot see the tags on tasks inside it and skips the include altogether. Tag the `include_role` task itself with `migrate`, and add `apply: tags: [migrate]` if you also want the tag on its child tasks - or switch to `import_role`, whose children inherit tags at parse time.
  • How does a when condition behave differently on import_role versus include_role?
    On `import_role` the condition is copied onto every task the role contributes and evaluated per task, so a false condition produces a long list of skipped tasks. On `include_role` it is evaluated once, on the include task, and a false result skips the entire role with a single line of output and no per-task evaluation.
  • How do roles listed under a play's roles: keyword differ from import_role in tasks:?
    The `roles:` header behaves like a static import but always runs before the play's `tasks:` section, regardless of where the header appears in the file. `import_role` inside `tasks:` runs at exactly the point you wrote it, which makes ordering explicit and is why task-level application is generally preferred in newer playbooks.

saying these in an interview costs you the question

  • Says the two are interchangeable aliases
  • Adds loop to import_role and expects it to work
  • Assumes --tags reaches tasks inside include_role
  • Thinks when on import_role is evaluated once at import
  • Believes roles: entries run wherever they appear in the file

context