AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 FQCNsansible.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 → rolestasks → 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 GuideAnsible Roles: Create Reusable AutomationAnsible import_tasks vs include_tasksAnsible Variable Precedence: Complete GuideAnsible Handlers: Trigger Actions on Change

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home