Mastering Dynamic Task Includes in Ansible
One of the best ways (and easy) to keep your Ansible playbooks clean, modular, and DRY (Don’t Repeat Yourself) is by using dynamic task includes. Instead of writing massive, conditional playbooks with dozens of when statements, you can let your data drive your execution.
The core idea is beautifully simple:
- name: Execute dynamically targeted action
ansible.builtin.include_tasks: "{{ action }}.yml"Depending on the value of the action variable (e.g., install, configure, or backup), Ansible will look for and execute install.yml, configure.yml, or backup.yml on the fly.
Why Use This Pattern?
- Cleaner Codebase: Instead of one massive
main.ymlpacked with tasks, you break your logic into small, focused files. - Role Extensibility: You can create “plugin-style” architectures. Want to support a new operating system or application action? Just drop a new
.ymlfile into the tasks directory. - Reduced Conditional Noise: You completely avoid appending
when: action == 'install'to fifteen different tasks.
⚠️ The Gotchas (What to Watch Out For)
While highly effective, include_tasks is evaluated at runtime, which introduces a couple of architectural quirks you need to design around.
1. The Missing File Trap
If action is set to restore, but restore.yml doesn’t exist, Ansible will fail mid-playbook execution.
The Fix: Use Ansible’s query with the first_found plugin to handle fallbacks or graceful skips.
- name: Include action tasks safely
ansible.builtin.include_tasks: "{{ item }}"
with_first_found:
- files:
- "{{ action }}.yml"
- "default_action.yml" # Fallback file
skip: true # Or skip entirely if neither existsPlease see my follow-up post Deep Dive Missing File Trap. “Dynamic task includes” is a classic, incredibly powerful Ansible pattern—but it’s also one that comes with a few hidden gotchas that can absolutely ruin a deployment if you aren’t prepared for them.
2. Handlers and Tags Limitations
Because dynamic includes aren’t parsed until the play actually reaches that task:
- You cannot easily trigger handlers defined inside a dynamically included file from outside of it.
- Running a playbook with
--tagsmight skip yourinclude_tasksentirely unless the include task itself carries the tag.
A Real-World Example: Multi-OS Configurations
A stellar use case for this is handling OS-specific setup without cluttering your main tasks file.
tasks/main.yml
- name: Load OS-specific configuration tasks
ansible.builtin.include_tasks: "{{ ansible_facts['os_family'] | lower }}.yml"tasks/debian.yml
- name: Install Debian/Ubuntu packages
ansible.builtin.apt:
name: Apache2
state: presenttasks/redhat.yml
- name: Install RedHat/CentOS packages
ansible.builtin.yum:
name: httpd
state: presentQuick Tip on Syntax: Notice the underscore ininclude_tasks. Older versions of Ansible allowed dashes (include-tasks), but modern Ansible strictly standardizes on underscores (include_tasks). Always use underscores to ensure your playbooks are future-proof!