Ansible Playbook Patterns: Writing Automation That Doesn't Break
Ansible is simple until it isn't. These patterns help you write playbooks that are maintainable, testable, and safe to run repeatedly.
February 23, 2026 · 6 min · 1235 words · Rob Washington
Table of Contents
Ansible’s simplicity is seductive. YAML tasks, SSH connections, no agents. But simple playbooks become complex fast, and poorly structured automation creates more problems than it solves.
These patterns help you write Ansible that scales with your infrastructure.
- name:Check if service is runningcommand:systemctl is-active myappregister:service_statuschanged_when:false# This is a check, not a changefailed_when:false# Don't fail if service is stopped- name:Start service if not runningservice:name:myappstate:startedwhen:service_status.stdout != "active"
Without changed_when: false, every check command shows as “changed” in output.
- name:Deploy with rollbackblock:- name:Deploy new versioncopy:src:app-v2.tar.gzdest:/opt/app/- name:Extract and startshell:| cd /opt/app && tar xzf app-v2.tar.gz
systemctl restart myapp- name:Verify deploymenturi:url:http://localhost:8080/healthstatus_code:200rescue:- name:Rollback to previous versionshell:| cd /opt/app && tar xzf app-v1.tar.gz
systemctl restart myapp- name:Alert on failuredebug:msg:"Deployment failed, rolled back to v1"always:- name:Clean up temp filesfile:path:/opt/app/*.tar.gzstate:absent
Ansible has many variable precedence levels. Use them intentionally:
1
2
3
4
5
6
7
8
9
10
# group_vars/all.ymlapp_port:8080app_user:appuserlog_level:info# group_vars/webservers.ymlnginx_worker_processes:auto# host_vars/web01.ymlnginx_worker_processes:4# Override for this host
Don’t hardcode values in playbooks. Parameterize everything.
--check: Don’t make changes, just report what would change
--diff: Show file content differences
Some tasks don’t support check mode. Mark them:
1
2
3
4
5
- name:Run database migrationcommand:/opt/app/migrate.shargs:creates:/opt/app/.migrated # Skip if file existscheck_mode:false# Always run, even in check mode
- name:Remove from load balanceruri:url:"http://lb.internal/api/remove"method:POSTbody:'{"host": "{{ inventory_hostname }}"}'delegate_to:localhost- name:Deploy to this hostcopy:src:app/dest:/opt/app/- name:Add back to load balanceruri:url:"http://lb.internal/api/add"method:POSTbody:'{"host": "{{ inventory_hostname }}"}'delegate_to:localhost
- hosts:webserversserial:2# Two hosts at a time# Or: serial: "25%" # 25% at a timetasks:- name:Deploy# ...- name:Verifyuri:url:"http://{{ inventory_hostname }}/health"delegate_to:localhost
If deployment fails on the first batch, remaining hosts are untouched.