Ansible import_role vs include_role — Static and Dynamic Role Loading

Introduction

Ansible provides two ways to load roles in tasks: ansible.builtin.import_role (static) and ansible.builtin.include_role (dynamic). The difference matters for tags, handlers, conditionals, and performance. Choosing wrong can lead to tasks being skipped unexpectedly, handlers not firing, or tags not working as expected.

Quick Comparison

Featureimport_role (Static)include_role (Dynamic)
Processing timeParse time (before play starts)Runtime (when task executes)
TagsInherited, work with --tagsNOT accessible via --tags
HandlersWork normallyWork normally
when conditionApplied to EVERY task in roleApplied once (include or skip all)
Loops❌ Cannot loop✅ Can loop
Variable filesLoaded at parse timeLoaded at runtime
PerformanceFaster (pre-parsed)Slightly slower
--list-tasksShows role tasksShows include statement only

import_role (Static)

---
- name: Static role import
  hosts: webservers
  tasks:
    - name: Configure base system
      ansible.builtin.import_role:
        name: common

    - name: Install web server
      ansible.builtin.import_role:
        name: nginx
        tasks_from: install    # Run specific task file

    - name: Configure web server
      ansible.builtin.import_role:
        name: nginx
        tasks_from: configure
      vars:
        nginx_port: 8080

Tags Work with import_role

    - name: Install monitoring
      ansible.builtin.import_role:
        name: monitoring
      tags:
        - monitoring
        - setup

# Tags are applied to ALL tasks inside the role
# ansible-playbook site.yml --tags monitoring  ← WORKS

when Applied to Every Task

    - name: Install Docker (Debian only)
      ansible.builtin.import_role:
        name: docker
      when: ansible_os_family == "Debian"
      # ⚠️ The `when` is applied to EVERY task inside the role
      # Each task evaluates the condition independently

include_role (Dynamic)

---
- name: Dynamic role inclusion
  hosts: all
  tasks:
    - name: Include role based on OS
      ansible.builtin.include_role:
        name: "{{ ansible_os_family | lower }}_base"
      # Role name can be a variable!

    - name: Include role conditionally
      ansible.builtin.include_role:
        name: special_config
      when: special_mode | default(false)
      # Condition evaluated ONCE — include all or nothing

Looping Over Roles

    - name: Install multiple applications
      ansible.builtin.include_role:
        name: "{{ app_role }}"
      loop:
        - redis
        - nginx
        - postgresql
      loop_control:
        loop_var: app_role
      # ✅ WORKS — include_role supports loops
      # ❌ import_role CANNOT loop

Dynamic Role Names

    - name: Include platform-specific role
      ansible.builtin.include_role:
        name: "platform_{{ ansible_system | lower }}"
      # Resolves to: platform_linux, platform_windows, etc.

    - name: Include roles from a list
      ansible.builtin.include_role:
        name: "{{ item }}"
      loop: "{{ required_roles }}"
      vars:
        required_roles:
          - security_hardening
          - log_shipping
          - backup_agent

Side-by-Side Examples

Scenario 1: Tag Filtering

# With import_role:
    - ansible.builtin.import_role:
        name: webserver
      tags: [web]
# ansible-playbook site.yml --tags web
# → Runs ALL tasks in webserver role ✅

# With include_role:
    - ansible.builtin.include_role:
        name: webserver
      tags: [web]
# ansible-playbook site.yml --tags web
# → Runs the include statement, BUT inner tasks need their own tags
# → May not work as expected ⚠️

Scenario 2: Conditional Execution

# With import_role:
    - ansible.builtin.import_role:
        name: firewall
      when: enable_firewall | default(true)
# ⚠️ Each task in the role checks `enable_firewall` independently
# If a task changes enable_firewall, later tasks see the change

# With include_role:
    - ansible.builtin.include_role:
        name: firewall
      when: enable_firewall | default(true)
# ✅ Condition checked ONCE — all tasks run or none run

Scenario 3: Variable Role Names

# import_role — role name MUST be static:
    - ansible.builtin.import_role:
        name: nginx          # ✅ String literal
    # name: "{{ my_var }}"   # ❌ Variables NOT allowed

# include_role — role name CAN be dynamic:
    - ansible.builtin.include_role:
        name: "{{ role_name }}"  # ✅ Variables allowed

Parameters

Both support the same parameters:

    - ansible.builtin.import_role:  # or include_role
        name: myapp
        tasks_from: deploy      # Run tasks/deploy.yml instead of tasks/main.yml
        vars_from: production   # Load vars/production.yml instead of vars/main.yml
        defaults_from: custom   # Load defaults/custom.yml
        handlers_from: custom   # Load handlers/custom.yml
        allow_duplicates: true  # Allow same role to run multiple times
        public: true            # Make role vars available after role completes
      vars:
        app_version: "2.0"

Decision Guide

Do you need --tags to filter role tasks?
  → YES: use import_role

Do you need a variable role name?
  → YES: use include_role

Do you need to loop over roles?
  → YES: use include_role

Do you want `when` to evaluate once (all-or-nothing)?
  → YES: use include_role

Do you want `when` on each task in the role?
  → YES: use import_role

Default choice?
  → import_role (simpler, tags work, visible in --list-tasks)

Roles Section vs Task-Based

# Traditional roles section (always static, like import_role)
- hosts: webservers
  roles:
    - common
    - nginx
    - { role: monitoring, tags: ['monitoring'] }

# Equivalent using import_role in tasks
- hosts: webservers
  tasks:
    - ansible.builtin.import_role:
        name: common
    - ansible.builtin.import_role:
        name: nginx
    - ansible.builtin.import_role:
        name: monitoring
      tags: [monitoring]

Troubleshooting

IssueSolution
--tags doesn't run role tasksSwitch from include_role to import_role
Can't use variable as role nameSwitch from import_role to include_role
when condition checked per-taskUse include_role for all-or-nothing conditional
Can't loop over rolesUse include_role (import_role doesn't support loops)
Role tasks not in --list-tasksDynamic includes don't show; use import_role for visibility

Best Practices

  1. Default to import_role — simpler, tags work, better visibility
  2. Use include_role for dynamic needs — variable names, loops, conditional includes
  3. Don't mix without reason — pick one style per project when possible
  4. Always use FQCNs — ansible.builtin.import_role, not just import_role
  5. Use tasks_from for modular roles — split large roles into focused task files

Conclusion

import_role is the safe default — it processes at parse time, works with tags, and shows up in --list-tasks. Use include_role when you need dynamic behavior: variable role names, loops, or all-or-nothing conditionals. Understanding the static/dynamic distinction prevents the most common Ansible role debugging headaches.