Introduction
Large Ansible projects split automation across multiple playbook files. import_playbook and task-level includes (import_tasks/include_tasks) are the two mechanisms for composing them. Understanding when each is processed — parse time vs runtime — is essential for conditional logic, tags, and debugging.
Quick Comparison
| Feature | import_playbook | include_tasks (in a play) |
|---|---|---|
| Processed at | Parse time (static) | Runtime (dynamic) |
Conditional (when) | ❌ Cannot conditionally import | ✅ Can conditionally include |
| Tags | ✅ Tags flow into imported plays | ⚠️ Tags on include apply to all tasks |
| Loops | ❌ Cannot loop | ✅ Can loop |
| Variables from runtime | ❌ Not available | ✅ Available |
| Error if file missing | ✅ Immediate error | ✅ Runtime error |
--list-tasks visibility | ✅ Shows all tasks | ❌ Shows include line only |
import_playbook — Static Inclusion
# site.yml — main entry point
---
- 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
roles:
- nginx
- app_deploy
- hosts: webservers
tasks:
- name: Verify deployment
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
Project Structure
ansible-project/
├── site.yml # Main entry point
├── playbooks/
│ ├── common.yml # Base setup for all hosts
│ ├── webservers.yml # Web server deployment
│ ├── dbservers.yml # Database setup
│ └── monitoring.yml # Monitoring agents
├── roles/
│ ├── common/
│ ├── nginx/
│ ├── postgresql/
│ └── deploy/
├── inventory/
│ ├── production/
│ └── staging/
└── group_vars/
Using Tags with import_playbook
# Run only web server tasks
ansible-playbook site.yml --tags webservers
# Skip database tasks
ansible-playbook site.yml --skip-tags database
# site.yml with tagged imports
- import_playbook: playbooks/common.yml
tags: common
- import_playbook: playbooks/webservers.yml
tags: webservers
# ⚠️ This tags the ENTIRE imported playbook
# All tasks inside get the 'webservers' tag
- import_playbook: playbooks/dbservers.yml
tags: database
Composing Plays Within Playbooks
# deploy.yml — multi-stage deployment
---
# Stage 1: Pre-deployment checks
- hosts: all
gather_facts: true
tasks:
- name: Validate environment
ansible.builtin.assert:
that: env is defined
# Stage 2: Database migration (run once)
- hosts: dbservers
tasks:
- name: Run migrations
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
# Stage 3: Rolling deploy
- hosts: webservers
serial: 2
roles:
- deploy
# Stage 4: Smoke tests
- hosts: webservers
tasks:
- name: Health check
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:{{ app_port }}/health"
Dynamic Task Inclusion Within Plays
# When you need runtime decisions, use include_tasks WITHIN a play
- hosts: webservers
tasks:
# Static import — processed at parse time
- import_tasks: tasks/validate.yml
# Dynamic include — processed at runtime
- include_tasks: "tasks/{{ ansible_os_family | lower }}.yml"
# Conditional include
- include_tasks: tasks/ssl-setup.yml
when: ssl_enabled | default(false)
# Looped include
- include_tasks: tasks/create-vhost.yml
loop: "{{ virtual_hosts }}"
loop_control:
loop_var: vhost
Environment-Based Composition
# site.yml — always runs
- import_playbook: playbooks/common.yml
- import_playbook: playbooks/security.yml
- import_playbook: playbooks/app.yml
# deploy-production.yml — production-specific
- import_playbook: playbooks/pre-deploy-checks.yml
- import_playbook: playbooks/backup.yml
- import_playbook: playbooks/app.yml
- import_playbook: playbooks/post-deploy-verify.yml
- import_playbook: playbooks/notify.yml
# deploy-staging.yml — simpler staging deploy
- import_playbook: playbooks/app.yml
- import_playbook: playbooks/post-deploy-verify.yml
Common Mistakes
# ❌ Trying to conditionally import a playbook
- import_playbook: playbooks/optional.yml
when: run_optional # This does NOT work as expected!
# 'when' is applied to every TASK inside, not the import itself
# ✅ Use separate entry point files instead
# Full deploy: ansible-playbook deploy-full.yml
# Quick deploy: ansible-playbook deploy-quick.yml
# ❌ import_playbook inside a play (not allowed)
- hosts: all
tasks:
- import_playbook: other.yml # SYNTAX ERROR!
# ✅ import_playbook is top-level only
- import_playbook: other.yml # Must be at the same level as '- hosts:'
# ❌ Deeply nested imports (hard to follow)
# site.yml → imports base.yml → imports sub-base.yml → imports...
# ✅ Keep it flat — max 2 levels deep
# site.yml → imports playbooks/*.yml (each is self-contained)
Debugging
# List all tasks (imports are expanded, includes are not)
ansible-playbook site.yml --list-tasks
# List all hosts
ansible-playbook site.yml --list-hosts
# Syntax check
ansible-playbook site.yml --syntax-check
# Dry run
ansible-playbook site.yml --check --diff
Related Articles
- Ansible include_tasks vs import_tasks
- Ansible Roles Guide
- Ansible Playbook Guide
- Ansible roles vs collections
Conclusion
import_playbook for static project composition — your site.yml imports webservers.yml, dbservers.yml, etc. It's processed at parse time, so tags and --list-tasks work perfectly. For runtime decisions within a play, use include_tasks (dynamic) or import_tasks (static). Keep project structure flat: one site.yml that imports focused playbook files, each targeting specific host groups. Create separate entry points (deploy-production.yml, deploy-staging.yml) instead of trying to conditionally import playbooks.