Introduction
As playbooks grow, splitting them into reusable files is essential. Ansible provides two mechanisms: import (static, parsed at load time) and include (dynamic, processed at runtime). Understanding the difference is key to organizing scalable automation.
import vs include
| import (static) | include (dynamic) | |
|---|---|---|
| When parsed | At playbook load time | At runtime when reached |
| Tags | Inherited by all tasks | Only on the include line |
| Conditionals | Applied to each task | Applied once to the include |
| Loops | ❌ Cannot loop | ✅ Can loop |
| Variables | Resolved at parse time | Resolved at runtime |
| Handlers | Can be notified by name | Cannot be notified by name |
| Best for | Predictable, always-run tasks | Conditional or looped includes |
import_tasks (Static)
# main.yml
---
- hosts: webservers
become: true
tasks:
- import_tasks: tasks/packages.yml
- import_tasks: tasks/config.yml
- import_tasks: tasks/services.yml
# tasks/packages.yml
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Install Redis
ansible.builtin.apt:
name: redis-server
state: present
Tags flow through to all imported tasks:
- import_tasks: tasks/packages.yml
tags: packages
# ALL tasks in packages.yml get the 'packages' tag
include_tasks (Dynamic)
# Conditional include
- include_tasks: tasks/debian.yml
when: ansible_os_family == 'Debian'
- include_tasks: tasks/redhat.yml
when: ansible_os_family == 'RedHat'
# Loop include
- include_tasks: tasks/setup-vhost.yml
loop: "{{ virtual_hosts }}"
loop_control:
loop_var: vhost
# Include with variables
- include_tasks: tasks/deploy.yml
vars:
app_version: "2.1.0"
app_env: production
Playbook workflow
Generate a complete playbook from this tutorial
Use Ansible include and import Tasks as the starting context and generate an editable Ansible playbook in the playground.
One anonymous generation is available. Do not paste passwords, private keys, tokens, or customer secrets.
import_playbook
# site.yml — master playbook
---
- import_playbook: playbooks/common.yml
- import_playbook: playbooks/webservers.yml
- import_playbook: playbooks/dbservers.yml
- import_playbook: playbooks/monitoring.yml
# playbooks/webservers.yml
---
- hosts: webservers
become: true
roles:
- nginx
- myapp
tasks:
- import_tasks: tasks/ssl.yml
include_role / import_role
# Static — parsed at load time
- import_role:
name: nginx
# Dynamic — processed at runtime
- include_role:
name: "{{ web_server }}" # Can use variables!
# With variables
- include_role:
name: myapp
vars:
app_port: 8080
app_env: production
# Conditional role
- include_role:
name: monitoring
when: enable_monitoring | default(true)
Practical Patterns
OS-Specific Tasks
# main.yml
- name: Include OS-specific tasks
include_tasks: "{{ ansible_os_family | lower }}.yml"
# Loads debian.yml or redhat.yml based on OS
# debian.yml
- ansible.builtin.apt:
name: nginx
state: present
update_cache: true
# redhat.yml
- ansible.builtin.yum:
name: nginx
state: present
Task File Per Component
project/
├── site.yml
├── tasks/
│ ├── users.yml
│ ├── packages.yml
│ ├── firewall.yml
│ ├── nginx.yml
│ ├── app-deploy.yml
│ └── monitoring.yml
# site.yml
---
- hosts: webservers
become: true
tasks:
- import_tasks: tasks/users.yml
tags: users
- import_tasks: tasks/packages.yml
tags: packages
- import_tasks: tasks/firewall.yml
tags: firewall
- import_tasks: tasks/nginx.yml
tags: nginx
- import_tasks: tasks/app-deploy.yml
tags: deploy
- import_tasks: tasks/monitoring.yml
tags: monitoring
Reusable Task with Parameters
# tasks/create-vhost.yml
- name: Create vhost directory
ansible.builtin.file:
path: "/var/www/{{ vhost.domain }}"
state: directory
owner: www-data
- name: Deploy vhost config
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-enabled/{{ vhost.domain }}.conf"
notify: Reload nginx
# main.yml
- include_tasks: tasks/create-vhost.yml
loop: "{{ virtual_hosts }}"
loop_control:
loop_var: vhost
Environment-Specific Playbooks
# deploy.yml
---
- import_playbook: playbooks/pre-deploy-checks.yml
- import_playbook: playbooks/deploy-app.yml
- import_playbook: playbooks/post-deploy-verify.yml
- import_playbook: playbooks/notify.yml
Handlers in Separate File
# handlers/main.yml
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
- name: Restart app
ansible.builtin.service:
name: myapp
state: restarted
# playbook.yml
- hosts: webservers
handlers:
- import_tasks: handlers/main.yml
tasks:
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
When to Use Which
Use import_tasks when:
- Tasks should always run
- You need tags to flow through
- You need handlers to be notifiable
- Task list is known at parse time
Use include_tasks when:
- Tasks are conditional (
when) - You need to loop over a task file
- Task file name comes from a variable
- You want lazy evaluation
Use import_playbook when:
- Composing a master playbook from smaller ones
- Each playbook targets different host groups
Use include_role when:
- Role selection is dynamic (variable-based)
- Role is conditionally applied
Troubleshooting
"Task not found" with Handlers and include_tasks
Handlers defined in include_tasks files can't be notified by name. Use import_tasks for handlers:
# WRONG
handlers:
- include_tasks: handlers.yml
# RIGHT
handlers:
- import_tasks: handlers.yml
Tags Not Working with include_tasks
Tags only apply to the include line itself, not inner tasks:
# Tags don't flow into included tasks
- include_tasks: tasks.yml
tags: mytag
# Tasks inside tasks.yml DON'T get 'mytag'
# Use import_tasks for tag inheritance
- import_tasks: tasks.yml
tags: mytag
# ALL tasks inside tasks.yml get 'mytag'
"Recursive loop detected"
A task file includes itself. Check for circular includes.
Related Articles
Conclusion
Use import_tasks (static) for predictable, always-run task files where tags and handlers need to work. Use include_tasks (dynamic) for conditional includes, loops, and variable-driven file selection. Use import_playbook to compose a master playbook from smaller targeted ones. The key rule: import = static (parse time), include = dynamic (runtime). Start with imports, switch to includes when you need dynamic behavior.