Skip to main content
Ansible advanced Lesson 4 of 5

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 async for 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

Frequently Asked Questions

What is the difference between include_tasks and import_tasks?
import_tasks is static—parsed at playbook load time, so conditionals apply to all tasks in the file. include_tasks is dynamic—resolved at run time, so each task in the included file can evaluate its own when condition independently.
How do I run Ansible faster?
Enable pipelining in ansible.cfg, use forks to run against more hosts in parallel, use async tasks for long-running operations, and consider using mitogen as the connection plugin for a significant speed boost.