Ansible Playbook Design Best Practices¶
Objective: Master Ansible playbook architecture for maintainable, scalable automation. When you need to build complex automation workflows, when you want reusable and testable code, when you're creating enterprise-grade automation—playbook design becomes your weapon of choice.
Ansible playbooks are the heart of automation. Proper playbook design enables maintainable code, reusable components, and scalable automation. This guide shows you how to wield playbook design with the precision of a DevOps engineer.
0) Prerequisites (Read Once, Live by Them)¶
The Five Commandments¶
- Understand playbook structure
- Play organization and task sequencing
- Role-based architecture and reusability
- Handler patterns and event-driven automation
-
Error handling and recovery procedures
-
Master role design
- Role structure and best practices
- Variable management and defaults
- Task organization and idempotency
-
Role dependencies and relationships
-
Know your testing patterns
- Unit testing with molecule
- Integration testing with testinfra
- Playbook validation and syntax checking
-
Continuous integration and deployment
-
Validate everything
- Test playbook syntax and structure
- Verify role functionality and dependencies
- Check variable resolution and defaults
-
Validate error handling and recovery
-
Plan for production
- Security and access controls
- Performance optimization and resource management
- Documentation and maintenance
- Monitoring and alerting
Why These Principles: Playbook design requires understanding both Ansible mechanics and software engineering patterns. Understanding these patterns prevents code duplication and enables maintainable automation.
1) Playbook Structure Design¶
Hierarchical Playbook Organization¶
# site.yml - Main playbook
---
- name: "Configure infrastructure"
import_playbook: infrastructure.yml
tags: [infrastructure]
- name: "Deploy applications"
import_playbook: applications.yml
tags: [applications]
- name: "Configure monitoring"
import_playbook: monitoring.yml
tags: [monitoring]
Play Organization¶
# infrastructure.yml
---
- name: "Configure base system"
hosts: all
become: yes
gather_facts: yes
vars:
system_packages:
- curl
- wget
- git
- vim
- htop
roles:
- common
- security
tags: [base, security]
- name: "Configure web servers"
hosts: webservers
become: yes
vars:
nginx_version: "1.20.2"
ssl_enabled: true
roles:
- nginx
- ssl
tags: [webservers, nginx, ssl]
- name: "Configure database servers"
hosts: databases
become: yes
vars:
postgresql_version: "13"
max_connections: 200
roles:
- postgresql
- backup
tags: [databases, postgresql, backup]
Why This Structure Matters: Hierarchical organization enables logical separation and reusability. Understanding these patterns prevents code duplication and enables maintainable automation.
2) Role Design Patterns¶
Role Structure¶
roles/
├── common/
│ ├── defaults/
│ │ └── main.yml
│ ├── vars/
│ │ └── main.yml
│ ├── tasks/
│ │ └── main.yml
│ ├── handlers/
│ │ └── main.yml
│ ├── templates/
│ │ └── nginx.conf.j2
│ ├── files/
│ │ └── authorized_keys
│ ├── meta/
│ │ └── main.yml
│ └── tests/
│ ├── inventory
│ └── test.yml
├── nginx/
│ ├── defaults/
│ ├── vars/
│ ├── tasks/
│ ├── handlers/
│ ├── templates/
│ ├── files/
│ ├── meta/
│ └── tests/
└── postgresql/
├── defaults/
├── vars/
├── tasks/
├── handlers/
├── templates/
├── files/
├── meta/
└── tests/
Role Defaults¶
# roles/nginx/defaults/main.yml
# Default variables for nginx role
nginx_version: "1.20.2"
nginx_worker_processes: "auto"
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
# SSL configuration
nginx_ssl_enabled: false
nginx_ssl_certificate: "/etc/ssl/certs/nginx.crt"
nginx_ssl_private_key: "/etc/ssl/private/nginx.key"
# Virtual hosts
nginx_vhosts: []
nginx_default_vhost: true
# Security settings
nginx_security_headers: true
nginx_hide_version: true
nginx_server_tokens: "off"
Role Variables¶
# roles/nginx/vars/main.yml
# Role-specific variables
nginx_packages:
- nginx
- nginx-common
- nginx-utils
nginx_service: nginx
nginx_config_dir: /etc/nginx
nginx_sites_dir: "{{ nginx_config_dir }}/sites-available"
nginx_sites_enabled_dir: "{{ nginx_config_dir }}/sites-enabled"
nginx_log_dir: /var/log/nginx
Role Tasks¶
# roles/nginx/tasks/main.yml
---
- name: "Install nginx packages"
package:
name: "{{ nginx_packages }}"
state: present
notify: restart nginx
- name: "Create nginx directories"
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: '0755'
loop:
- "{{ nginx_log_dir }}"
- "{{ nginx_sites_dir }}"
- "{{ nginx_sites_enabled_dir }}"
- name: "Configure nginx main config"
template:
src: nginx.conf.j2
dest: "{{ nginx_config_dir }}/nginx.conf"
owner: root
group: root
mode: '0644'
notify: restart nginx
- name: "Configure nginx virtual hosts"
include_tasks: vhosts.yml
loop: "{{ nginx_vhosts }}"
when: nginx_vhosts | length > 0
- name: "Enable nginx service"
systemd:
name: "{{ nginx_service }}"
enabled: yes
state: started
Role Handlers¶
# roles/nginx/handlers/main.yml
---
- name: restart nginx
systemd:
name: "{{ nginx_service }}"
state: restarted
- name: reload nginx
systemd:
name: "{{ nginx_service }}"
state: reloaded
- name: test nginx config
command: nginx -t
changed_when: false
failed_when: false
Why Role Design Matters: Proper role structure enables reusability and maintainability. Understanding these patterns prevents code duplication and enables scalable automation.
3) Task Organization¶
Task Idempotency¶
# Idempotent task examples
- name: "Install packages"
package:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
- name: "Create user"
user:
name: "{{ username }}"
shell: /bin/bash
home: "/home/{{ username }}"
create_home: yes
when: username is defined
- name: "Configure service"
template:
src: service.conf.j2
dest: "/etc/service/service.conf"
owner: root
group: root
mode: '0644'
notify: restart service
Error Handling¶
# Error handling patterns
- name: "Attempt risky operation"
command: "risky-command"
register: result
failed_when: false
changed_when: false
- name: "Handle failure gracefully"
debug:
msg: "Operation failed, continuing with fallback"
when: result.rc != 0
- name: "Retry on failure"
command: "flaky-command"
register: result
retries: 3
delay: 5
until: result.rc == 0
Conditional Logic¶
# Conditional task execution
- name: "Install development packages"
package:
name: "{{ dev_packages }}"
state: present
when: environment == "development"
- name: "Configure production settings"
template:
src: production.conf.j2
dest: "/etc/app/production.conf"
when: environment == "production"
- name: "Enable debug logging"
lineinfile:
path: "/etc/app/app.conf"
line: "debug = true"
when: debug_enabled | default(false)
Why Task Organization Matters: Proper task organization enables reliable automation and error handling. Understanding these patterns prevents deployment failures and enables maintainable automation.
4) Variable Management¶
Variable Precedence¶
# Variable precedence hierarchy
# 1. Command line variables (-e)
# 2. Role defaults
# 3. Inventory group_vars
# 4. Inventory host_vars
# 5. Playbook group_vars
# 6. Playbook host_vars
# 7. Host facts
# 8. Registered variables
# 9. Set_fact variables
# 10. Extra variables (-e)
Variable Validation¶
# Variable validation patterns
- name: "Validate required variables"
assert:
that:
- nginx_version is defined
- nginx_worker_processes is defined
- nginx_worker_connections is defined
fail_msg: "Required nginx variables are not defined"
- name: "Validate variable types"
assert:
that:
- nginx_worker_connections is number
- nginx_ssl_enabled is boolean
- nginx_vhosts is list
fail_msg: "Variable types are incorrect"
- name: "Set default values"
set_fact:
nginx_worker_processes: "{{ nginx_worker_processes | default('auto') }}"
nginx_worker_connections: "{{ nginx_worker_connections | default(1024) }}"
Variable Scoping¶
# Variable scoping examples
- name: "Set play-level variables"
set_fact:
play_variable: "play_value"
delegate_to: localhost
- name: "Set host-level variables"
set_fact:
host_variable: "host_value"
- name: "Set group-level variables"
set_fact:
group_variable: "group_value"
delegate_to: "{{ groups['webservers'] }}"
Why Variable Management Matters: Proper variable management enables flexible configuration and prevents errors. Understanding these patterns prevents configuration conflicts and enables maintainable automation.
5) Testing and Validation¶
Molecule Testing¶
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: instance
image: ubuntu:20.04
pre_build_image: true
provisioner:
name: ansible
inventory:
host_vars:
instance:
test_variable: "test_value"
verifier:
name: testinfra
Testinfra Tests¶
# molecule/default/tests/test_default.py
import testinfra
def test_nginx_installed(host):
"""Test that nginx is installed"""
nginx = host.package("nginx")
assert nginx.is_installed
def test_nginx_running(host):
"""Test that nginx is running"""
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_config(host):
"""Test nginx configuration"""
nginx_config = host.file("/etc/nginx/nginx.conf")
assert nginx_config.exists
assert nginx_config.user == "root"
assert nginx_config.group == "root"
assert nginx_config.mode == 0o644
def test_nginx_port(host):
"""Test that nginx is listening on port 80"""
nginx_port = host.socket("tcp://0.0.0.0:80")
assert nginx_port.is_listening
Playbook Validation¶
# Syntax validation
ansible-playbook --syntax-check playbook.yml
# Dry run validation
ansible-playbook --check playbook.yml
# Variable validation
ansible-playbook --check --diff playbook.yml
# Tag-based validation
ansible-playbook --check --tags nginx playbook.yml
Why Testing Matters: Proper testing enables reliable automation and prevents deployment failures. Understanding these patterns prevents production issues and enables maintainable automation.
6) Performance Optimization¶
Task Optimization¶
# Optimized task patterns
- name: "Install multiple packages in one task"
package:
name: "{{ packages }}"
state: present
vars:
packages:
- nginx
- curl
- wget
- git
- name: "Use async for long-running tasks"
command: "long-running-command"
async: 300
poll: 10
register: long_task
- name: "Use delegate_to for local operations"
command: "local-command"
delegate_to: localhost
run_once: true
Parallel Execution¶
# Parallel execution patterns
- name: "Configure multiple hosts in parallel"
template:
src: config.j2
dest: "/etc/app/config"
delegate_to: "{{ item }}"
loop: "{{ groups['webservers'] }}"
async: 60
poll: 0
- name: "Wait for all tasks to complete"
wait_for:
timeout: 300
when: async_tasks is defined
Resource Management¶
# Resource management patterns
- name: "Limit concurrent tasks"
command: "resource-intensive-command"
throttle: 2
async: 60
poll: 0
- name: "Use serial for ordered execution"
command: "ordered-command"
serial: 1
when: ordered_execution is defined
Why Performance Optimization Matters: Proper optimization enables efficient automation and resource utilization. Understanding these patterns prevents resource exhaustion and enables scalable automation.
7) Security Best Practices¶
Privilege Escalation¶
# Privilege escalation patterns
- name: "Run with specific user"
command: "user-command"
become: yes
become_user: "{{ service_user }}"
become_method: sudo
- name: "Run without privilege escalation"
command: "user-command"
become: no
- name: "Run with specific sudo options"
command: "sudo-command"
become: yes
become_flags: "--preserve-env"
Secret Management¶
# Secret management patterns
- name: "Use encrypted variables"
template:
src: config.j2
dest: "/etc/app/config"
vars:
secret_value: "{{ vault_secret_value }}"
- name: "Use external secret management"
uri:
url: "https://vault.example.com/v1/secret/ansible"
method: GET
headers:
X-Vault-Token: "{{ vault_token }}"
register: secret_response
Access Control¶
# Access control patterns
- name: "Restrict task execution"
command: "sensitive-command"
when:
- ansible_user == "admin"
- environment == "production"
become: yes
become_user: root
Why Security Matters: Proper security practices prevent unauthorized access and ensure compliance. Understanding these patterns prevents security breaches and enables secure automation.
8) Best Practices Summary¶
Playbook Design Principles¶
# Essential playbook patterns
playbook_patterns:
structure: "Hierarchical organization with logical separation"
roles: "Reusable components with clear interfaces"
tasks: "Idempotent operations with proper error handling"
variables: "Clear precedence with validation and defaults"
testing: "Comprehensive testing with molecule and testinfra"
Maintenance Procedures¶
# Regular maintenance tasks
maintenance:
syntax: "ansible-playbook --syntax-check playbook.yml"
validation: "ansible-playbook --check playbook.yml"
testing: "molecule test"
documentation: "Update README and inline comments"
cleanup: "Remove unused tasks and variables"
Red Flags¶
# Playbook anti-patterns
red_flags:
hardcoded_values: "Never hardcode configuration values"
non_idempotent: "Avoid non-idempotent tasks"
poor_error_handling: "Always handle errors gracefully"
missing_validation: "Validate all inputs and variables"
poor_documentation: "Document all complex logic and decisions"
Why Best Practices Matter: Proper playbook design enables maintainable, scalable automation. Understanding these patterns prevents code duplication and enables reliable infrastructure management.
9) TL;DR Quickstart¶
Essential Commands¶
# Syntax check
ansible-playbook --syntax-check playbook.yml
# Dry run
ansible-playbook --check playbook.yml
# Run playbook
ansible-playbook playbook.yml
# With tags
ansible-playbook --tags nginx playbook.yml
Essential Patterns¶
# Essential playbook patterns
playbook_patterns:
structure: "Hierarchical with logical separation"
roles: "Reusable components with clear interfaces"
tasks: "Idempotent operations with error handling"
variables: "Clear precedence with validation"
testing: "Comprehensive testing with molecule"
Why This Quickstart: These patterns cover 90% of playbook design needs. Master these before exploring advanced features.
10) The Machine's Summary¶
Ansible playbook design requires understanding both automation patterns and software engineering principles. When used correctly, playbook design enables maintainable, scalable automation that can handle complex infrastructure requirements. The key is understanding role architecture, mastering testing patterns, and following best practices.
The Dark Truth: Without proper playbook understanding, your automation is fragile and unmaintainable. Playbooks are your weapon. Use them wisely.
The Machine's Mantra: "In structure we trust, in roles we reuse, and in the playbook we find the path to maintainable automation."
Why This Matters: Playbook design enables reliable infrastructure automation that can handle complex deployments, maintain code quality, and provide scalable automation while ensuring maintainability and testability.
This guide provides the complete machinery for Ansible playbook design. The patterns scale from simple single-task playbooks to complex multi-role deployments, from basic automation to advanced enterprise patterns.