Ansible is easy to start, hard to scale. Here’s how to structure playbooks that don’t become unmaintainable nightmares.
Directory Structure# Start organized, stay organized:
a ├ ├ │ │ │ │ │ │ │ │ │ ├ │ │ │ ├ │ │ │ └ n ─ ─ ─ ─ ─ s ─ ─ ─ ─ ─ i b a i ├ │ │ │ │ │ └ p ├ ├ └ r ├ ├ └ c l n n ─ ─ l ─ ─ ─ o ─ ─ ─ o e s v ─ ─ a ─ ─ ─ l ─ ─ ─ l / i e y e l b n p ├ └ s ├ └ b s w d s c n p e l t r ─ ─ t ─ ─ o i e a / o g o c e o o ─ ─ a ─ ─ o t b t m i s t . r d g k e s a m n t i c y u h g ├ ├ └ i h g s . e b o x g o f / c o r ─ ─ ─ n o r y r a n r n g t s o ─ ─ ─ g s m v s e s i t u / t u l e e s / o s p a w d s p r s q n . _ l e a . _ s . l y v l b t y v . y / m a . s a m a y m l r y e b l r m l s m r a s l / l v s / e e r s s . . y y m m l l Inventory Patterns# Static YAML Inventory# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# inventory/production/hosts.yml
all :
children :
webservers :
hosts :
web1.example.com :
web2.example.com :
vars :
http_port : 80
databases :
hosts :
db1.example.com :
postgresql_port : 5432
db2.example.com :
postgresql_port : 5433
Dynamic Inventory# For cloud infrastructure, use dynamic inventory:
1
2
3
4
5
6
7
8
9
10
11
12
# AWS EC2
ansible-inventory -i aws_ec2.yml --list
# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
Group Variables Hierarchy# g ├ ├ └ r ─ ─ ─ o ─ ─ ─ u p a w p _ l e r v l b o a . s d r y e u s m r c / l v t e i r o s n . . y y m m l l # # # A W P l e r l b o s d h e u o r c s v t t e i s r o n g r e o n u v p i r o n m e n t
Variables merge with precedence:
a l l < g r o u p < h o s t < p l a y b o o k < e x t r a v a r s
Role Design# Minimal Role Structure# r ├ │ ├ │ ├ │ ├ │ └ o ─ ─ ─ ─ ─ l ─ ─ ─ ─ ─ e s d └ t └ h └ t └ m └ / e ─ a ─ a ─ e ─ e ─ n f ─ s ─ n ─ m ─ t ─ g a k d p a i u m s m l m l n / m n l a / a e a a g a x t i i r i t i i / s n n s n e n n / . . / . s x . y y y / . y m m m c m l l l o l n f . j 2 # # # # # D M H J D e a a i e f i n n p a n d j e u l a n l t e 2 d t a r e s s t n v k e c a ( m i r l r p e i i e l s a s s a , b t t t l a e m e r s e s t t , a ( d l r a o e t w l a e o s a t d ) p r e c e d e n c e )
defaults/main.yml# Sensible defaults that users can override:
1
2
3
4
5
# roles/nginx/defaults/main.yml
nginx_worker_processes : auto
nginx_worker_connections : 1024
nginx_keepalive_timeout : 65
nginx_server_names_hash_bucket_size : 64
tasks/main.yml# Keep it focused:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# roles/nginx/tasks/main.yml
---
- name : Include OS-specific variables
include_vars : "{{ ansible_os_family }}.yml"
- name : Install nginx
package :
name : nginx
state : present
- name : Configure nginx
template :
src : nginx.conf.j2
dest : /etc/nginx/nginx.conf
validate : nginx -t -c %s
notify : Restart nginx
- name : Ensure nginx is running
service :
name : nginx
state : started
enabled : true
handlers/main.yml# 1
2
3
4
5
6
7
8
9
10
11
# roles/nginx/handlers/main.yml
---
- name : Restart nginx
service :
name : nginx
state : restarted
- name : Reload nginx
service :
name : nginx
state : reloaded
Task Patterns# Idempotency# Every task should be safe to run multiple times:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Good: Idempotent
- name : Ensure directory exists
file :
path : /opt/myapp
state : directory
mode : '0755'
# Bad: Not idempotent (creates duplicates)
- name : Add line to file
shell : echo "export PATH=/opt/bin:$PATH" >> ~/.bashrc
# Good: Idempotent line management
- name : Add to PATH
lineinfile :
path : ~/.bashrc
line : 'export PATH=/opt/bin:$PATH'
state : present
Conditionals# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name : Install on Debian
apt :
name : nginx
when : ansible_os_family == "Debian"
- name : Install on RedHat
yum :
name : nginx
when : ansible_os_family == "RedHat"
# Or use package module (cross-platform)
- name : Install nginx
package :
name : nginx
state : present
Loops# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Simple loop
- name : Create users
user :
name : "{{ item }}"
state : present
loop :
- alice
- bob
- charlie
# Loop with dictionaries
- name : Create users with groups
user :
name : "{{ item.name }}"
groups : "{{ item.groups }}"
loop :
- { name: alice, groups : admin }
- { name: bob, groups : developers }
Blocks for Error Handling# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- name : Deploy application
block :
- name : Download artifact
get_url :
url : "{{ artifact_url }}"
dest : /tmp/app.tar.gz
- name : Extract artifact
unarchive :
src : /tmp/app.tar.gz
dest : /opt/app
remote_src : yes
rescue :
- name : Rollback on failure
copy :
src : /opt/app.backup
dest : /opt/app
remote_src : yes
always :
- name : Clean up temp files
file :
path : /tmp/app.tar.gz
state : absent
Variable Management# Vault for Secrets# 1
2
3
4
5
6
7
8
# Create encrypted file
ansible-vault create secrets.yml
# Edit encrypted file
ansible-vault edit secrets.yml
# Use in playbook
ansible-playbook site.yml --ask-vault-pass
1
2
3
# group_vars/all/secrets.yml (encrypted)
db_password : supersecret
api_key : abc123
Variable Precedence Strategy# 1
2
3
4
5
6
7
8
9
# defaults/main.yml - Role defaults (override freely)
nginx_port : 80
# vars/main.yml - Role internals (don't override)
nginx_config_path : /etc/nginx/nginx.conf
# group_vars/ - Environment-specific
# host_vars/ - Host-specific
# --extra-vars - Runtime overrides (highest precedence)
Playbook Organization# Site Playbook# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# playbooks/site.yml
---
- name : Apply common configuration
hosts : all
roles :
- common
- security
- name : Configure web servers
hosts : webservers
roles :
- nginx
- app
- name : Configure databases
hosts : databases
roles :
- postgresql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name : Install packages
package :
name : nginx
tags :
- install
- nginx
- name : Configure nginx
template :
src : nginx.conf.j2
dest : /etc/nginx/nginx.conf
tags :
- configure
- nginx
1
2
3
4
5
# Run only nginx tasks
ansible-playbook site.yml --tags nginx
# Skip installation
ansible-playbook site.yml --skip-tags install
Testing# Molecule for Role Testing# 1
2
3
4
5
6
# Initialize molecule scenario
cd roles/nginx
molecule init scenario -d docker
# Run tests
molecule test
1
2
3
4
5
6
7
8
9
10
11
12
# molecule/default/molecule.yml
dependency :
name : galaxy
driver :
name : docker
platforms :
- name : instance
image : ubuntu:22.04
provisioner :
name : ansible
verifier :
name : ansible
Ansible-lint# 1
ansible-lint playbooks/site.yml
1
2
3
4
# .ansible-lint
skip_list :
- yaml[line-length]
- name[casing]
Parallelism# 1
2
3
# ansible.cfg
[defaults]
forks = 20 # Parallel hosts
Pipelining# 1
2
[ssh_connection]
pipelining = True # Reduces SSH operations
Async Tasks# 1
2
3
4
5
6
7
8
9
10
11
12
- name : Long running task
command : /opt/scripts/backup.sh
async : 3600 # Max runtime in seconds
poll : 0 # Don't wait
- name : Check backup status
async_status :
jid : "{{ backup_job.ansible_job_id }}"
register : job_result
until : job_result.finished
retries : 60
delay : 60
Caching Facts# 1
2
3
4
5
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
Common Mistakes# 1. Hardcoded Values# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Bad
- name : Configure app
template :
src : app.conf.j2
dest : /etc/myapp/app.conf
owner : appuser
group : appuser
# Good
- name : Configure app
template :
src : app.conf.j2
dest : "{{ app_config_path }}"
owner : "{{ app_user }}"
group : "{{ app_group }}"
2. Missing Handlers# 1
2
3
4
5
6
7
8
9
10
11
12
# Bad: Service not restarted after config change
- name : Update config
template :
src : app.conf.j2
dest : /etc/app.conf
# Good: Notify handler
- name : Update config
template :
src : app.conf.j2
dest : /etc/app.conf
notify : Restart app
3. Command Module Overuse# 1
2
3
4
5
6
7
8
9
# Bad: Using command when module exists
- name : Install package
command : apt-get install -y nginx
# Good: Use the module
- name : Install package
apt :
name : nginx
state : present
The Ansible Checklist# Start simple, add complexity only when needed. The best Ansible code is boring and predictable.
Ansible’s power is simplicity. The goal isn’t clever automation — it’s reliable, readable automation that your team can maintain.