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
| Feature | import_role (Static) | include_role (Dynamic) |
|---|---|---|
| Processing time | Parse time (before play starts) | Runtime (when task executes) |
| Tags | Inherited, work with --tags | NOT accessible via --tags |
| Handlers | Work normally | Work normally |
when condition | Applied to EVERY task in role | Applied once (include or skip all) |
| Loops | ❌ Cannot loop | ✅ Can loop |
| Variable files | Loaded at parse time | Loaded at runtime |
| Performance | Faster (pre-parsed) | Slightly slower |
--list-tasks | Shows role tasks | Shows 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
| Issue | Solution |
|---|---|
--tags doesn't run role tasks | Switch from include_role to import_role |
| Can't use variable as role name | Switch from import_role to include_role |
when condition checked per-task | Use include_role for all-or-nothing conditional |
| Can't loop over roles | Use include_role (import_role doesn't support loops) |
Role tasks not in --list-tasks | Dynamic includes don't show; use import_role for visibility |
Best Practices
- Default to
import_role— simpler, tags work, better visibility - Use
include_rolefor dynamic needs — variable names, loops, conditional includes - Don't mix without reason — pick one style per project when possible
- Always use FQCNs —
ansible.builtin.import_role, not justimport_role - Use
tasks_fromfor 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.