Ansible Playbook Patterns: Idempotent Infrastructure Done Right
Battle-tested Ansible patterns for writing maintainable, reusable, and idempotent playbooks.
February 25, 2026 · 10 min · 2060 words · Rob Washington
Table of Contents
Ansible’s simplicity is deceptive. Anyone can write a playbook that works once. Writing playbooks that work reliably, repeatedly, and maintainably requires discipline and patterns.
# handlers/main.yml- name:Reload nginxservice:name:nginxstate:reloadedlisten:"reload web server"- name:Restart nginxservice:name:nginxstate:restartedlisten:"restart web server"# In tasks- name:Update nginx configtemplate:src:nginx.conf.j2dest:/etc/nginx/nginx.confnotify:"reload web server"
- name:Install nginxapt:name:nginxstate:presentnotify:Start nginx- name:Flush handlersmeta:flush_handlers- name:Configure nginxtemplate:src:nginx.conf.j2dest:/etc/nginx/nginx.conf# nginx is now guaranteed to be running
# Use bool filter for string booleans- name:Enable debug modetemplate:src:debug.conf.j2dest:/etc/app/debug.confwhen:debug_mode | bool# Check if variable is defined and not empty- name:Set custom configcopy:content:"{{ custom_config }}"dest:/etc/app/custom.confwhen:- custom_config is defined- custom_config | length > 0# Multiple conditions- name:Deploy to productioninclude_tasks:deploy.ymlwhen:- env == 'production'- deploy_enabled | bool- inventory_hostname in groups['webservers']
- name:Get current versioncommand:cat /opt/app/VERSIONregister:current_versionchanged_when:falsecheck_mode:false# Always run, even in check mode- name:Deploy new versionunarchive:src:"app-{{ target_version }}.tar.gz"dest:/opt/app/when:current_version.stdout != target_version
- name:Run database migrationcommand:./manage.py migrate --checkregister:migration_checkchanged_when:"'No migrations to apply' not in migration_check.stdout"failed_when:migration_check.rc not in [0, 1]
# Bad - not idempotent- name:Create directorycommand:mkdir -p /opt/app# Good - idempotent- name:Create directoryfile:path:/opt/appstate:directorymode:'0755'
- hosts:webserversgather_facts:falsetasks:- name:Quick task without factsping:# Or gather specific facts- hosts:webserversgather_subset:- network- hardware
# molecule/default/verify.yml- name:Verifyhosts:alltasks:- name:Check nginx is runningservice:name:nginxstate:startedcheck_mode:trueregister:resultfailed_when:result.changed
1
2
# Run testsmolecule test
Good Ansible is boring Ansible. No surprises, no side effects, same result every time. When your playbooks are truly idempotent, running them becomes a confidence-builder rather than a risk.
Start with roles, use handlers properly, validate your templates, and test with Molecule. Your future self—and your 3 AM pager—will thank you.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.