Ansible Course
Playbooks Introduction
In this lesson
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.
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.
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.
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.
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.
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.
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
hosts,
become, vars, and gather_facts apply
to every task in the play unless a task overrides them individually.
--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.
site.yml
guarantees that Play 2 only starts after Play 1 is complete on all hosts,
enabling reliable sequencing of dependent services.
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.