Ansible Lesson 12 – Playbooks Introduction | Dataplexa
Section II · Lesson 12

Playbooks Introduction

In this lesson

Playbook anatomy Plays vs tasks Running a playbook Play recap output Multiple plays

A playbook is a YAML file that describes a complete automation workflow — which hosts to target, in what order to run tasks, and what each task should do. It is Ansible's primary unit of reusable, version-controlled automation. Where an ad-hoc command runs one module once, a playbook orchestrates dozens of tasks across multiple host groups in a defined sequence, with error handling, conditionals, and reporting built in. Every serious Ansible project is driven by playbooks — understanding their structure is the foundation of everything in Section II.

Playbook Anatomy

A playbook is a list of plays. Each play targets a group of hosts and contains a list of tasks. Each task calls one module. This three-level hierarchy — playbook → play → task — is the entire structural grammar of Ansible automation.

PLAYBOOK (site.yml) Play 1 — webservers Task: Install Nginx ansible.builtin.package Task: Copy config file ansible.builtin.template Task: Start Nginx ansible.builtin.service Task: Open firewall port ansible.builtin.firewalld Play 2 — databases Task: Install PostgreSQL ansible.builtin.package Task: Create database user community.postgresql.user Task: Start PostgreSQL ansible.builtin.service Task: Configure pg_hba.conf ansible.builtin.template

Play Attributes

Every play begins with a set of attributes that configure how it runs — who it targets, how it connects, whether to escalate privileges, and what to do when a task fails. These attributes apply to every task in the play unless explicitly overridden at the task level.

Common play-level attributes

name A human-readable description of the play. Shows in output during the run. Required — always name your plays.
hosts The inventory host pattern this play runs against. Can be a group name, a single host, a wildcard, or all. Required on every play.
become Whether to escalate privileges for all tasks in this play. Overrides the ansible.cfg default. Set to true when the play needs root access.
vars A mapping of variables scoped to this play. Available to all tasks within the play. For larger variable sets, prefer vars_files or group_vars/.
gather_facts Whether to run the setup module at the start of the play to collect host facts. Default: true. Set to false to speed up plays that do not need facts.
serial How many hosts to run the play against at once. Useful for rolling deployments — e.g. serial: 1 updates one server at a time, keeping the others live.
ignore_errors When true, a failed task does not stop the play. Ansible continues to the next task. Use with caution — failed tasks usually mean the system is not in the state you intended.

The Film Production Analogy

A playbook is like a film script. The script (playbook) contains multiple scenes (plays). Each scene has a cast (hosts), a setting (become, vars), and a sequence of actions (tasks). The director (Ansible) follows the script scene by scene, giving each actor (module) their cue in order. If an actor flubs their lines (task fails), the director can choose to stop the production or carry on to the next scene — that choice is ignore_errors.

A Complete Annotated Playbook

Here is a full, working playbook that installs and configures Nginx on all web servers. Every line is annotated to explain both the syntax and the intent — read through it top to bottom before moving on.

---
# Every playbook starts with --- (YAML document marker)

# A playbook is a list — the first item is the first play
- name: Configure web servers          # play name: shown in output
  hosts: webservers                    # target: all hosts in the webservers group
  become: true                         # all tasks in this play run as root via sudo
  gather_facts: true                   # collect host info before running tasks

  vars:
    nginx_port: 80                     # play-scoped variable — available to all tasks
    server_name: "example.com"

  tasks:
    # Task 1 — install the package
    - name: Install Nginx web server
      ansible.builtin.package:
        name: nginx
        state: present                 # install if missing; skip if already installed

    # Task 2 — write the config file from a template
    - name: Deploy Nginx configuration
      ansible.builtin.template:
        src: nginx.conf.j2             # Jinja2 template on the control node
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
      notify: Restart Nginx            # trigger the handler if this task changes

    # Task 3 — ensure the service is running and enabled on boot
    - name: Ensure Nginx is started and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  # Handlers run once at the end of the play, only if notified
  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

What just happened?

This playbook targets the webservers group, runs all tasks as root, installs Nginx if it is missing, deploys a config file from a template, and ensures the service is running. The notify/handler pattern means Nginx is only restarted if the config file actually changed — a second run with no config changes produces zero changes and no restart. That is idempotency working exactly as intended.

Running a Playbook

Playbooks are executed with the ansible-playbook command — not ansible. The most common invocation patterns are shown below, starting from the simplest and building up to the options you will use daily in production.

# Basic run — uses ansible.cfg for inventory and connection settings
ansible-playbook site.yml

# Specify inventory explicitly
ansible-playbook -i inventory/production/hosts.ini site.yml

# Dry run — show what WOULD change without making any changes
ansible-playbook site.yml --check

# Dry run with diff — also shows the exact content changes for files
ansible-playbook site.yml --check --diff

# Limit to a single host or group (override the hosts: in the play)
ansible-playbook site.yml --limit web01.example.com
ansible-playbook site.yml --limit webservers

# Increase verbosity for debugging (-v, -vv, -vvv)
ansible-playbook site.yml -v

# Pass extra variables at runtime
ansible-playbook site.yml -e "nginx_port=8080 server_name=staging.example.com"
PLAY [Configure web servers] **************************************************

TASK [Gathering Facts] ********************************************************
ok: [web01.example.com]
ok: [web02.example.com]

TASK [Install Nginx web server] ***********************************************
changed: [web01.example.com]
ok: [web02.example.com]        <-- already installed on web02

TASK [Deploy Nginx configuration] *********************************************
changed: [web01.example.com]
changed: [web02.example.com]

TASK [Ensure Nginx is started and enabled] ************************************
ok: [web01.example.com]
ok: [web02.example.com]

RUNNING HANDLERS [Configure web servers] **************************************
changed: [web01.example.com]   <-- restarted because config changed
changed: [web02.example.com]   <-- restarted because config changed

PLAY RECAP ****************************************************
web01.example.com   : ok=5  changed=3  unreachable=0  failed=0
web02.example.com   : ok=4  changed=2  unreachable=0  failed=0

Reading the play recap

The PLAY RECAP is the most important line in any playbook run. ok means the system was already correct. changed means Ansible made a modification. failed=0 means everything succeeded. unreachable=0 means all hosts were reachable. A clean production run should show only ok and zero failures — changed tasks on a re-run mean your playbook is not fully idempotent yet.

Multiple Plays in One Playbook

A single playbook file can contain multiple plays, each targeting a different host group. This is the pattern used for a master site.yml that configures an entire environment — web servers, databases, and load balancers — in a single run with a guaranteed execution order.

---
# Play 1 — configure web servers first
- name: Configure web servers
  hosts: webservers
  become: true
  tasks:
    - name: Install Nginx
      ansible.builtin.package:
        name: nginx
        state: present

# Play 2 — configure databases after web servers are done
- name: Configure database servers
  hosts: databases
  become: true
  tasks:
    - name: Install PostgreSQL
      ansible.builtin.package:
        name: postgresql
        state: present

# Play 3 — run a smoke test against localhost after everything else
- name: Run post-deployment health checks
  hosts: localhost              # run on the control node, not a managed node
  gather_facts: false           # no need to collect facts for a local play
  tasks:
    - name: Verify web servers are responding
      ansible.builtin.uri:
        url: "http://{{ item }}"
        status_code: 200
      loop: "{{ groups['webservers'] }}"

Plays execute in order — Play 2 will not start until every host in Play 1 has completed (or failed). This ordering guarantee is what makes a multi-play site.yml reliable for coordinating dependent infrastructure components.

Task Status Meanings

Every task in a playbook run produces one of five status values. Understanding each one tells you exactly what happened on the managed node — and what to do about it.

ok

No change needed

The system was already in the desired state. The module checked, found nothing to do, and returned successfully. This is the ideal outcome on a re-run.

changed

Change applied

The module found the system in a different state than desired and made a modification. Expected on the first run; should be zero on subsequent runs for a fully idempotent playbook.

failed

Task error

The module encountered an error and could not complete the task. Ansible stops the play for that host by default. The play recap shows failed=1 or higher.

skipped

Condition not met

The task had a when: condition that evaluated to false for this host. The module was not called at all. Not an error — working as designed.

unreachable

Connection failed

Ansible could not establish an SSH connection to the host. All tasks for that host are skipped. Check SSH key authentication, firewall rules, and whether the host is online. This is always a network or authentication problem, never a playbook bug.

A Clean Play Recap Does Not Mean Your Playbook Is Correct

failed=0 in the play recap means every task reported success — it does not mean the system is in the state you intended. A task that uses ansible.builtin.shell with a wrong command can report changed and exit 0 while having done something unexpected. Always verify the actual state of your managed nodes after a first run, especially for playbooks that contain command or shell tasks.

Key Takeaways

A playbook is a list of plays — each play targets a host group and contains a list of tasks. The three-level hierarchy is playbook → play → task, and every element at each level is a YAML mapping.
Play attributes configure the entire playhosts, become, vars, and gather_facts apply to every task in the play unless a task overrides them individually.
Use --check --diff before every production run — dry run shows what would change, diff shows the exact content of file changes. Together they are your pre-flight confirmation.
Plays execute in order — a multi-play site.yml guarantees that Play 2 only starts after Play 1 is complete on all hosts, enabling reliable sequencing of dependent services.
The play recap is your primary run summary — read ok, changed, failed, and unreachable after every run. A fully idempotent playbook on its second run should show only ok counts and no failures.

Teacher's Note

Copy the annotated playbook from this lesson into your project, change the hosts to match your inventory, and run it — then run it a second time and compare the play recap. Seeing changed=0 on the second run is the moment idempotency stops being a theory and becomes something you have personally observed.

Practice Questions

1. Which command is used to execute an Ansible playbook file?



2. What flag runs a playbook in dry-run mode — showing what would change without applying any changes?



3. Which play attribute defines which inventory hosts or groups the play runs against?



Quiz

1. A playbook has two plays: Play 1 targets webservers and Play 2 targets databases. In what order do they execute?


2. A task shows a status of skipped in the playbook output. What does this mean?


3. You have a short play that runs a single health check and does not need any information about the target host. Which play attribute should you set to make it run faster?


Up Next · Lesson 13

Writing Your First Playbook

Stop reading and start writing — Lesson 13 takes you from a blank file to a fully working playbook that provisions a real web server from scratch.