Advanced Ansible Playbook Patterns
Master loops, conditionals, error handling, dynamic inventory, and performance patterns to write production-grade Ansible playbooks.
Once you have roles and variables working, the next step is writing robust playbooks that handle real-world complexity: iteration, conditions, failure recovery, and scalability.
Learning outcomes
By the end you can:
- loop over lists and dictionaries
- apply conditional logic with
when - handle errors gracefully with
block/rescue/always - use
asyncfor long-running tasks - generate and use dynamic inventory
1) Loops — iterate over lists
Simple list loop
- name: Install multiple packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- curl
- unzip
Loop over a list of dictionaries
- name: Create application users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: "{{ item.shell | default('/bin/bash') }}"
state: present
loop:
- { name: deploy, groups: www-data }
- { name: monitor, groups: adm }
- { name: backup, groups: sudo }
Loop with index
- name: Print each item with its index
ansible.builtin.debug:
msg: "Item {{ ansible_loop.index }}: {{ item }}"
loop: "{{ my_list }}"
loop_control:
extended: true
2) Conditionals — when clause
Run a task only when a condition is true:
- name: Install apache2 (Debian/Ubuntu only)
ansible.builtin.apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install httpd (RedHat/CentOS only)
ansible.builtin.yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
Multiple conditions (AND):
- name: Configure production database
ansible.builtin.template:
src: db_prod.conf.j2
dest: /etc/app/db.conf
when:
- app_env == "production"
- ansible_memory_mb.real.total >= 4096
Test if a variable is defined:
- name: Set custom port only if defined
ansible.builtin.lineinfile:
path: /etc/app/config
line: "port={{ custom_port }}"
when: custom_port is defined
3) Registering and using task output
- name: Check if app config exists
ansible.builtin.stat:
path: /etc/app/config.yml
register: app_config
- name: Backup existing config before overwriting
ansible.builtin.copy:
src: /etc/app/config.yml
dest: /etc/app/config.yml.bak
remote_src: true
when: app_config.stat.exists
- name: Deploy new config
ansible.builtin.template:
src: config.yml.j2
dest: /etc/app/config.yml
4) Error handling — block/rescue/always
block groups tasks. rescue runs if any block task fails. always runs regardless.
- name: Deploy application with rollback on failure
block:
- name: Stop the service
ansible.builtin.service:
name: myapp
state: stopped
- name: Deploy new version
ansible.builtin.copy:
src: myapp-v2.tar.gz
dest: /opt/myapp/
- name: Extract new version
ansible.builtin.unarchive:
src: /opt/myapp/myapp-v2.tar.gz
dest: /opt/myapp/
remote_src: true
- name: Start the service
ansible.builtin.service:
name: myapp
state: started
rescue:
- name: Rollback to previous version
ansible.builtin.command:
cmd: /opt/myapp/rollback.sh
- name: Notify team of failure
ansible.builtin.debug:
msg: "Deployment failed! Rollback executed."
always:
- name: Record deployment attempt
ansible.builtin.shell:
cmd: echo "$(date) - deployment attempted" >> /var/log/deploys.log
5) Async tasks — for long-running operations
Long operations (package installs, large file copies) can time out the SSH connection.
async runs the task in the background, poll checks progress.
- name: Upgrade all packages (may take a while)
ansible.builtin.apt:
upgrade: dist
update_cache: true
async: 600 # max run time: 10 minutes
poll: 15 # check every 15 seconds
register: upgrade_result
- name: Wait for upgrade to finish
ansible.builtin.async_status:
jid: "{{ upgrade_result.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 40
delay: 15
6) Dynamic inventory
Instead of a static inventory.ini, a dynamic inventory script or plugin queries your cloud/CMDB in real time.
AWS EC2 dynamic inventory (example)
Install the AWS collection:
ansible-galaxy collection install amazon.aws
Create aws_ec2.yml:
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- eu-west-1
filters:
instance-state-name: running
tag:Env: production
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.region
prefix: region
Run:
ansible-inventory -i aws_ec2.yml --list
ansible-playbook -i aws_ec2.yml site.yml
Ansible automatically groups hosts by their tags.
7) Performance: parallel execution
By default Ansible runs against 5 hosts in parallel (forks = 5).
Increase in ansible.cfg:
[defaults]
forks = 20
pipelining = true
pipelining = true reduces SSH connections and speeds up execution significantly.
8) Tagging tasks for selective runs
Tag tasks so you can run specific parts of a playbook:
- name: Install packages
ansible.builtin.apt:
name: nginx
state: present
tags:
- install
- nginx
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- config
- nginx
Run only tagged tasks:
# Only run install tasks
ansible-playbook site.yml --tags install
# Run everything except config tasks
ansible-playbook site.yml --skip-tags config
Next steps
- Running Ansible in CI/CD pipelines (GitHub Actions, Jenkins)
- Ansible Tower / AWX for team-scale automation
- Testing playbooks with Molecule