| name | ansible-playbook-design |
| description | Patterns for designing well-structured, maintainable Ansible playbooks with state-based (present/absent) design, play organization, and proper variable scoping.
|
| when_to_use | Use when creating new Ansible playbooks, designing playbook structure, implementing state-based playbooks with present/absent patterns, organizing plays and tasks, or structuring playbook variables.
|
Ansible Playbook Design
Patterns for designing well-structured, maintainable Ansible playbooks.
State-Based Playbook Pattern
Design playbooks to handle both creation and removal via a state variable.
Core Pattern
---
- name: Manage admin user account
hosts: all
become: true
vars:
admin_state: present
tasks:
- name: Create admin user
ansible.builtin.user:
name: "{{ admin_name }}"
groups: "{{ admin_groups }}"
state: "{{ admin_state }}"
- name: Configure SSH key
ansible.posix.authorized_key:
user: "{{ admin_name }}"
key: "{{ admin_ssh_key }}"
state: "{{ admin_state }}"
when: admin_state == 'present'
Usage
uv run ansible-playbook playbooks/manage-admin.yml \
-e "admin_name=alice" \
-e "admin_ssh_key='ssh-ed25519 AAAA...'"
uv run ansible-playbook playbooks/manage-admin.yml \
-e "admin_name=alice" \
-e "admin_state=absent"
Benefits
- Single source of truth
- Consistent interface
- Less code duplication
- Follows community role conventions
Play Structure
Recommended Play Sections
Order sections consistently across all playbooks:
---
- name: Descriptive play name
hosts: target_group
become: true
gather_facts: true
vars:
app_version: "2.0.0"
vars_files:
- vars/secrets.yml
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
roles:
- role: common
- role: app_deploy
vars:
deploy_version: "{{ app_version }}"
tasks:
- name: Verify deployment
ansible.builtin.uri:
url: http://localhost:8080/health
post_tasks:
- name: Send deployment notification
ansible.builtin.debug:
msg: "Deployment complete"
handlers:
- name: restart app
ansible.builtin.systemd:
name: myapp
state: restarted
Variable Organization
Variable Precedence (Key Levels)
From lowest to highest precedence:
- Role defaults (
roles/x/defaults/main.yml)
- Inventory group_vars (
group_vars/all.yml)
- Inventory host_vars (
host_vars/hostname.yml)
- Play vars (
vars: in playbook)
- Task vars (
vars: on task)
- Extra vars (
-e on command line) - highest
Organizing Variables
ansible/
├── group_vars/
│ ├── all.yml # Variables for ALL hosts
│ ├── proxmox.yml # Proxmox cluster hosts
│ └── docker_hosts.yml # Docker host group
├── host_vars/
│ ├── node01.yml # Host-specific overrides
│ └── node02.yml
└── playbooks/
└── deploy.yml # Uses vars: for playbook-specific
Variable Naming by Scope
default_timezone: "UTC"
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
proxmox_api_host: "192.168.1.10"
proxmox_cluster_name: "production"
proxmox_node_id: 1
ceph_osd_devices:
- /dev/sdb
- /dev/sdc
Task Organization with Includes
When to Split Tasks
Split playbook tasks into separate files when:
- Tasks exceed 50 lines
- Logical groupings emerge (networking, storage, users)
- Conditional sections can be skipped entirely
Include Patterns
---
- name: Setup Proxmox cluster
hosts: proxmox
become: true
tasks:
- name: Configure networking
ansible.builtin.include_tasks: tasks/networking.yml
- name: Setup storage
ansible.builtin.include_tasks: tasks/storage.yml
when: setup_storage | default(true)
- name: Initialize cluster
ansible.builtin.include_tasks: tasks/cluster-init.yml
when: inventory_hostname == groups['proxmox'][0]
import_tasks vs include_tasks
| Feature | import_tasks | include_tasks |
|---|
| When evaluated | Parse time (static) | Runtime (dynamic) |
| Supports loops | No | Yes |
| Supports conditionals on import | Limited | Full |
| Use case | Ordered execution | Conditional/looped |
- ansible.builtin.import_tasks: users.yml
- ansible.builtin.import_tasks: permissions.yml
- ansible.builtin.include_tasks: "setup-{{ ansible_os_family }}.yml"
- ansible.builtin.include_tasks: deploy-app.yml
loop: "{{ applications }}"
Multi-Play Playbooks
Use multiple plays for different host groups or privilege levels:
---
- name: Gather cluster information
hosts: proxmox
gather_facts: true
tasks:
- name: Set cluster facts
ansible.builtin.set_fact:
cluster_node_count: "{{ groups['proxmox'] | length }}"
- name: Initialize cluster on primary
hosts: proxmox[0]
become: true
tasks:
- name: Create cluster
ansible.builtin.command: pvecm create {{ cluster_name }}
when: not cluster_exists
- name: Join cluster on secondary nodes
hosts: proxmox[1:]
become: true
serial: 1
tasks:
- name: Join cluster
ansible.builtin.command: pvecm add {{ primary_node }}
when: not node_in_cluster
Handler Best Practices
Define Handlers at Play Level
---
- name: Configure web server
hosts: webservers
become: true
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
- name: Update SSL certificates
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/nginx/ssl/
loop:
- cert.pem
- key.pem
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Handler Execution Order
Handlers run:
- At the end of each play
- In the order they are defined (not notified)
- Only once, even if notified multiple times
Force immediate handler execution:
- name: Update critical config
ansible.builtin.template:
src: config.j2
dest: /etc/app/config.yml
notify: restart app
- name: Flush handlers now
ansible.builtin.meta: flush_handlers
- name: Verify app is running
ansible.builtin.uri:
url: http://localhost:8080/health
Playbook Validation
Pre-flight Checks
Add validation at the start of playbooks:
---
- name: Deploy application
hosts: app_servers
become: true
tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- app_version is defined
- app_version | regex_search('^\d+\.\d+\.\d+$')
- deploy_env in ['staging', 'production']
fail_msg: "Invalid configuration. Check app_version and deploy_env."
- name: Check disk space
ansible.builtin.assert:
that: ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first > 1073741824
fail_msg: "Insufficient disk space. Need at least 1GB free."
Template Patterns
Playbook Template Structure
---
- name: [Descriptive play name]
hosts: [target_group]
become: [true/false]
gather_facts: [true/false]
vars:
resource_state: present
tasks:
- name: Validate inputs
ansible.builtin.assert:
that:
- required_var is defined
fail_msg: "required_var must be defined"
- name: Verify completion
ansible.builtin.debug:
msg: "Playbook completed successfully"
Additional Resources
For detailed playbook patterns and techniques, consult:
references/playbook-role-patterns.md - Comprehensive playbook organization patterns, play structure, import strategies
Related Skills
- ansible-role-design - When to use roles vs playbooks
- ansible-fundamentals - Core module selection and naming
- ansible-error-handling - Block/rescue patterns in playbooks