Ansible Playbook Anatomy

A playbook is a YAML file containing one or more plays. Each play targets a set of hosts and defines a sequence of tasks to run on them. Understanding the relationship between playbooks, plays, tasks, and modules is the conceptual breakthrough that makes everything else in Ansible click into place.

The Hierarchy: Playbook → Play → Task → Module

Think of it as nested containers: a playbook holds plays, plays hold tasks, and each task invokes exactly one module. This hierarchy maps directly onto how you think about automation: "On my web servers (play), install nginx (task using apt module), start nginx (task using service module), and deploy the config file (task using template module)."

A Complete Annotated Playbook

---
# The three dashes mark the start of a YAML document
# A playbook is a list of plays

- name: Configure Web Servers          # Play name - shown in output, should be descriptive
  hosts: webservers                    # Which hosts from inventory to target
  become: true                         # Use privilege escalation (sudo) for all tasks
  gather_facts: true                   # Collect system facts before tasks run (default)
  
  vars:                                # Play-level variables
    http_port: 80
    document_root: /var/www/html
  
  tasks:                               # The ordered list of tasks in this play
  
    - name: Ensure Nginx is installed  # Task name - shown in output
      apt:                             # Module name
        name: nginx                    # Module argument
        state: present                 # Desired state
        update_cache: true             # Additional argument
    
    - name: Ensure Nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: true
    
    - name: Create document root directory
      file:
        path: "{{ document_root }}"    # Using a variable with Jinja2 syntax
        state: directory
        owner: www-data
        mode: '0755'
    
    - name: Deploy index.html
      copy:
        content: "<h1>Server {{ inventory_hostname }} is ready</h1>"
        dest: "{{ document_root }}/index.html"
        owner: www-data
        mode: '0644'
      notify: Reload Nginx             # Trigger the handler named "Reload Nginx"

  handlers:                            # Tasks that run only when notified
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

- name: Configure Database Servers    # Second play in the same playbook
  hosts: databases
  become: true
  
  tasks:
    - name: Install PostgreSQL
      apt:
        name: postgresql
        state: present

The Play

A play has these key components:

  • name — A human-readable description. Required by convention, not technically mandatory, but essential for readable output.
  • hosts — Specifies which inventory hosts this play targets. Can be a group name, a hostname, a comma-separated list, a wildcard pattern, or all.
  • become — Enable privilege escalation for all tasks in this play.
  • gather_facts — Whether to collect system facts before tasks (default true). Set to false to speed up plays that do not need facts.
  • vars — Variables scoped to this play.
  • tasks — The ordered list of tasks.
  • handlers — Tasks triggered by notifications from other tasks.

The Task

Every task has a name and invokes one module with its arguments. Task names appear in playbook output and in Tower/AWX logs — write them as clear, present-tense descriptions of what the task does. "Ensure Nginx is installed" is better than "nginx" or "install stuff".

Tasks run in order, one at a time, on all targeted hosts. If a task fails on a host, that host is removed from the remaining tasks (unless you configure error handling — covered in Module 4).

The Module

A module is the implementation of a specific type of operation. When you write apt: in a task, you are invoking the apt module. The module arguments below it (name, state, update_cache) are the parameters that configure what the module does. Every module has its own set of arguments documented in the Ansible module index.

Special Variables Available in Playbooks

  • inventory_hostname — The name of the current host as defined in inventory (web01, db01, etc.)
  • ansible_host — The actual IP or hostname used for connection
  • ansible_facts — Dictionary of all gathered facts for the current host
  • hostvars — Access variables from other hosts: hostvars['web01']['ansible_ip_addresses']
  • groups — Dictionary of all inventory groups and their member hosts
  • play_hosts — List of hosts active in the current play

Running a Playbook

ansible-playbook -i inventory.ini site.yml

Common useful flags:

  • --check — Dry run mode: shows what would change without making changes
  • --diff — Shows the diff for file changes
  • --limit web01 — Run only against a specific host
  • --tags install — Run only tasks tagged "install"
  • --start-at-task "Task Name" — Start from a specific task
  • -v — Verbose output

Try This: Read a Playbook Out Loud

Take the annotated playbook above and read it aloud as a narrative: "On my web servers, with sudo enabled... first ensure nginx is installed... then ensure nginx is running and enabled on boot..." This exercise reinforces that a well-written playbook is nearly self-documenting and helps you internalise the task sequence pattern you will use throughout your Ansible career.

Summary

A playbook contains one or more plays. Each play targets a set of hosts and contains an ordered list of tasks. Each task invokes one module with specific arguments. The play controls overall settings like privilege escalation, fact gathering, and variables. Handlers are special tasks triggered only when other tasks report a change. Understanding this hierarchy is the conceptual foundation of all Ansible automation.

Leave a Comment