Ansible Playbook Structure: Anatomy, Best Practices & Examples (2026)
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible playbook structure. Understand plays, tasks, handlers, variables, roles, includes, and project organization with practical examples.
An Ansible playbook is a YAML file containing one or more plays that define automation tasks for target hosts. Understanding the structure is fundamental — it determines how your automation executes, scales, and stays maintainable.
Basic Playbook Structure
---
# A playbook is a list of plays
- name: First play - Configure web servers # Play 1
hosts: webservers
become: true
vars:
http_port: 80
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Second play - Configure databases # Play 2
hosts: databases
become: true
tasks:
- name: Install PostgreSQL
ansible.builtin.package:
name: postgresql
state: present
See also: Ansible Tags: Run Specific Tasks and Roles Selectively (Guide)
Anatomy of a Play
Every play has these key sections:
- name: Descriptive play name # Optional but recommended
hosts: target_group # REQUIRED: which hosts to target
become: true # Privilege escalation (sudo)
become_user: root # Who to become (default: root)
gather_facts: true # Gather system info (default: true)
connection: ssh # Connection type (default: ssh)
serial: 5 # Rolling update batch size
max_fail_percentage: 10 # Fail threshold
# Variables
vars:
key: value
vars_files:
- vars/config.yml
vars_prompt:
- name: username
prompt: "Enter username"
# Pre-tasks (run before roles)
pre_tasks:
- name: Update package cache
ansible.builtin.apt:
update_cache: true
# Roles
roles:
- common
- webserver
# Tasks (run after roles)
tasks:
- name: Deploy application
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
# Post-tasks (run after tasks)
post_tasks:
- name: Verify deployment
ansible.builtin.uri:
url: http://localhost/health
# Handlers (triggered by notify)
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Execution Order Within a Play
pre_tasks
Handlers triggered by pre_tasks
roles
tasks
Handlers triggered by roles and tasks
post_tasks
Handlers triggered by post_tasks
Task Structure
tasks:
- name: Descriptive task name # Always name your tasks
ansible.builtin.module_name: # Module to use (FQCN recommended)
param1: value1
param2: value2
register: result # Save output to variable
when: condition # Conditional execution
loop: "{{ list }}" # Iterate over items
become: true # Per-task privilege escalation
notify: handler_name # Trigger handler on change
tags: ['tag1', 'tag2'] # Tags for selective execution
changed_when: false # Override changed detection
failed_when: result.rc > 1 # Override failure detection
ignore_errors: true # Continue on failure
no_log: true # Hide output (secrets)
delegate_to: localhost # Run on different host
run_once: true # Run once for entire play
retries: 3 # Retry on failure
delay: 5 # Delay between retries
until: result is success # Retry condition
See also: Ansible Variable Precedence: Complete Order of Priority (2026)
Handlers
Handlers run only when triggered by notify and only when the notifying task reports "changed":
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- validate nginx
- restart nginx
handlers:
- name: validate nginx
ansible.builtin.command: nginx -t
changed_when: false
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Variables
Variable Sources (in a playbook)
- hosts: webservers
# Inline variables
vars:
http_port: 80
max_clients: 200
# From files
vars_files:
- vars/common.yml
- "vars/{{ env }}.yml"
# Interactive prompts
vars_prompt:
- name: deploy_version
prompt: "Version to deploy"
default: "latest"
private: false
tasks:
# Task-level variables
- name: Deploy app
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
vars:
extra_setting: task_only_value
# Runtime variables
- name: Set computed variable
ansible.builtin.set_fact:
full_url: "http://{{ ansible_hostname }}:{{ http_port }}"
See also: Ansible block, rescue, always: Error Handling Complete Guide (2026)
Conditionals
tasks:
# Simple condition
- name: Install on Debian
ansible.builtin.apt:
name: nginx
when: ansible_os_family == 'Debian'
# Multiple conditions (AND)
- name: Restart if config changed and not in maintenance
ansible.builtin.service:
name: app
state: restarted
when:
- config_result is changed
- not maintenance_mode
# OR condition
- name: Alert on critical
ansible.builtin.debug:
msg: "Alert!"
when: disk_usage > 90 or memory_usage > 95
Loops
tasks:
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, groups: admin }
- { name: bob, groups: developers }
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- redis
- postgresql
Roles
Roles package tasks, variables, files, and templates into reusable units:
- hosts: webservers
roles:
# Simple role
- common
# Role with variables
- role: webserver
vars:
http_port: 8080
# Conditional role
- role: monitoring
when: enable_monitoring | default(true)
# Tagged role
- role: security
tags: ['security', 'hardening']
Role Directory Structure
roles/webserver/
├── defaults/main.yml # Default variables (easily overridden)
├── vars/main.yml # Role variables (hard to override)
├── tasks/main.yml # Tasks
├── handlers/main.yml # Handlers
├── templates/ # Jinja2 templates
├── files/ # Static files
├── meta/main.yml # Role metadata and dependencies
└── README.md
Project Organization
Small Project
project/
├── inventory.yml
├── playbook.yml
├── vars/
│ ├── common.yml
│ └── secrets.yml
└── templates/
└── config.j2
Medium Project
project/
├── ansible.cfg
├── inventory/
│ ├── production.yml
│ └── staging.yml
├── group_vars/
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
├── host_vars/
│ └── web1.yml
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
└── files/
Large Project (Recommended)
project/
├── ansible.cfg
├── requirements.yml # Collection dependencies
├── inventory/
│ ├── production/
│ │ ├── hosts.yml
│ │ ├── group_vars/
│ │ └── host_vars/
│ └── staging/
│ ├── hosts.yml
│ ├── group_vars/
│ └── host_vars/
├── playbooks/
│ ├── site.yml # Master playbook
│ ├── webservers.yml
│ ├── databases.yml
│ └── monitoring.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ ├── database/
│ └── monitoring/
├── plugins/
│ └── filter/
├── collections/
└── docs/
Master Playbook (site.yml)
---
- ansible.builtin.import_playbook: playbooks/common.yml
- ansible.builtin.import_playbook: playbooks/webservers.yml
- ansible.builtin.import_playbook: playbooks/databases.yml
- ansible.builtin.import_playbook: playbooks/monitoring.yml
Best Practices
Always name plays and tasks — descriptive names make debugging easy Use FQCNs —ansible.builtin.copy not just copy
Use roles for reusability — don't put everything in one playbook
Keep secrets in Vault — use vars_files with encrypted files
Use tags — enable selective execution: ansible-playbook site.yml --tags deploy
Use changed_when/failed_when — make command/shell tasks idempotent
One task, one purpose — avoid complex shell scripts in tasks
Test with --check — dry-run before applying changes
FAQ
What is the basic structure of an Ansible playbook?
A playbook is a YAML file containing a list of plays. Each play specifies target hosts (hosts), optional settings (become, vars), and a list of tasks with modules to execute. A minimal playbook needs just hosts and tasks.
What is the difference between a play and a task?
A play maps a group of hosts to tasks and defines the execution context (become, vars, connection). A task is a single action that calls an Ansible module with specific parameters. A play contains multiple tasks.
What order do tasks execute in a play?
pre_tasks → handlers → roles → tasks → handlers → post_tasks → handlers. Within each section, tasks execute top-to-bottom in the order they're written.
How do I organize a large Ansible project?
Use roles for reusable components, separate inventories per environment, group_vars/host_vars for environment-specific config, a master site.yml that imports per-component playbooks, and requirements.yml for collection dependencies.
Should I use one big playbook or many small ones?
Use many small, focused playbooks organized by component or function (web, database, monitoring). Import them into a master site.yml with import_playbook. This enables selective execution and keeps each file manageable.
Conclusion
A well-structured Ansible playbook: • Uses descriptive names for plays and tasks • Separates concerns with roles and multiple playbooks • Keeps variables in group_vars/host_vars and vars_files • Uses handlers for triggered actions (restarts, reloads) • Follows the right project layout for your team's size
Start simple and refactor into roles as your automation grows.
Related Articles
• Ansible Playbook Examples: Complete Guide • Ansible Roles: Create Reusable Automation • Ansible import_tasks vs include_tasks • Ansible Variable Precedence: Complete Guide • Ansible Handlers: Trigger Actions on ChangeCategory: installation