Check a playbook before running it
On a correct playbook --syntax-check prints only playbook: site.yml.
--list-tasks lists the tasks in order with their tags:
Runs an Ansible playbook on the inventory machines and reports ok, changed, skipping or failed for each task.
ansible-playbook -i inventory.ini site.yml
run
ansible-playbook -i inventory.ini site.yml -C -D
what would change
ansible-playbook -i inventory.ini site.yml --syntax-check
ansible-playbook -i inventory.ini site.yml --list-tasks
ansible-playbook -i inventory.ini site.yml -l node2
one machine
ansible-playbook -i inventory.ini site.yml -t page
tagged tasks
ansible-playbook -i inventory.ini site.yml -e site_title=Demo
ansible-playbook -i inventory.ini site.yml -v
more detail
ansible-playbook takes a playbook — a YAML file with plays — and an
inventory, connects to the machines and runs the tasks in order. What an
inventory, modules and one-off ansible commands are is in the
ansible article. The lab machine has ansible-core 2.16 and the
ansible 9.2 package with 103 collections (ansible-galaxy collection
list); there is no network to the outside, so collections cannot be
installed from Galaxy.
The playbook for the examples is site.yml:
The template templates/index.html.j2 sits next to the playbook —
template looks for src in the templates/ directory:
The inventory inventory.ini is one machine, this one, without SSH:
The first run (the stars in the headers are shortened, the summary line is wrapped):
Gathering Facts collects facts about the machine (ansible_hostname
in the template comes from here). The state of a task on each machine:
| State | What it means |
|---|---|
ok |
the machine is already as described — nothing changed |
changed |
the task changed the machine |
skipping |
the task was skipped: a when condition or --check for shell |
fatal … FAILED! |
an error; no more tasks run on this machine |
UNREACHABLE! |
cannot connect to the machine |
PLAY RECAP is a line per machine: how many tasks ended in which state.
| Field | What it counts |
|---|---|
ok |
tasks with and without changes — all that went through |
changed |
tasks that changed the machine (also counted in ok) |
unreachable |
the machine is unreachable — the tasks never ran |
failed |
failed tasks; after the first one nothing more runs on this machine |
skipped |
skipped tasks |
rescued, ignored |
errors caught by rescue: or ignore_errors: true |
A good repeat run is changed=0 failed=0. The exit code: 0 — all went
well, 2 — a task failed on some machine, 4 — a machine is unreachable or
the playbook could not be parsed (a YAML error, an unknown module), 1 —
another error, for example no machines left.
The file and template modules first look at the machine and change
only the difference — this is idempotency. shell
does not know what its command does and always reports changed. A good
playbook gives changed=0 on the second run: for the task — a module
instead of shell, or creates: (do not run if the file exists), or
changed_when: (your own rule for what counts as a change).
A handler is a task that runs only if another task called it with
notify and that task reported changed. In the first run the template
changed — nginx reloaded its config; in the second the template was
ok, and there is no RUNNING HANDLER in the output.
notify looks up a handler by its name (or by listen) exactly,
case-sensitively; a typo is the error
ansible-playbook.--force-handlers runs them anyway. To run them before the end, use a
task ansible.builtin.meta: flush_handlers.The template module builds a file from a template
.j2: each {{ name }} is replaced with a variable's value. It compares
the result with what is on the machine and reports changed only if they
differ — that is why a template and its handler give changed=0 on a
repeat run.
Where the values come from:
vars: — site_title: Demo;Gathering Facts — ansible_hostname
(node1);[group:vars];-e site_title=Changed from the command line — above all the others.A variable that does not exist fails the task —
ansible-playbook. A default right in the template
— {{ site_title | default('Demo') }}. --check --diff shows what the
template would change.
--check (-C) runs the playbook without changing anything; --diff
(-D) prints what would change in files:
changed here means "would change": the file stayed the same, and
nobody touched nginx. shell and command are skipped in check mode —
--check tells you nothing about them. A playbook for which
--check --diff after a run shows no changed at all describes the
machine as it is.
The main options are in the table below; the full list is
ansible-playbook --help. Without -i Ansible looks for an inventory
in /etc/ansible/hosts, and the lab machine has none — pass the
inventory explicitly.
| Option | What it does | Example |
|---|---|---|
-i --inventory FILE |
The inventory — a file listing machines and groups. | ansible-playbook -i inventory.ini site.yml |
-C --check |
A run without changes — what would change; shell and command are skipped. | ansible-playbook -i inventory.ini site.yml --check |
-D --diff |
Show the difference in files and templates, usually together with --check. | ansible-playbook -i inventory.ini site.yml --check --diff |
-l --limit PATTERN |
Only these machines or groups from the inventory. | ansible-playbook -i inventory.ini site.yml -l node2 |
-e --extra-vars VARS |
Variables with the highest precedence — key=value or @file. | ansible-playbook -i inventory.ini site.yml -e site_title=Demo |
-b --become |
Run tasks through sudo — like become true in the playbook. | ansible-playbook -i inventory.ini site.yml -b |
-t --tags TAGS |
Only tasks with these tags. | ansible-playbook -i inventory.ini site.yml -t page |
--syntax-check |
Only parse the playbook; on a correct one it prints its name. | ansible-playbook -i inventory.ini site.yml --syntax-check |
--list-tasks |
The tasks in order with their tags, without a run. | ansible-playbook -i inventory.ini site.yml --list-tasks |
--list-hosts |
Which machines the playbook will go to, without a run. | ansible-playbook -i inventory.ini site.yml --list-hosts |
-v --verbose |
More detail — each task's result; -vvv adds the SSH connection. | ansible-playbook -i inventory.ini site.yml -vvv |
--skip-tags TAGS |
All tasks except those with these tags. | ansible-playbook -i inventory.ini site.yml --skip-tags page |
--start-at-task NAME |
Start at the task with this name, skipping the ones before it. | ansible-playbook -i inventory.ini site.yml --start-at-task 'Stamp' |
--step |
Ask before each task — run it, skip it, or continue without asking. | ansible-playbook -i inventory.ini site.yml --step |
--force-handlers |
Run the handlers that were called even if a task on the machine failed. | ansible-playbook -i inventory.ini site.yml --force-handlers |
-u --user USER |
Which user to log in to the machines as over SSH. | ansible-playbook -i inventory.ini site.yml -u deploy |
Nothing found. Try a shorter query.
On a correct playbook --syntax-check prints only playbook: site.yml.
--list-tasks lists the tasks in order with their tags:
The inventory node2.ini:
Ansible logs in over SSH under the same name as you: as student when
run by student, as root under sudo. student on node1 has a key
to node2 (~/.ssh/id_ed25519), and sudo there needs no password, so
become: true in a playbook works on node2 too. These are the
machines of a two-VM lab; a lesson machine has no node2. For another
name use ansible_user=<name> on the machine's line or -u <name>.
With -t page the fact gathering, the tagged task and its handler ran —
ok=3 changed=2.
-e site_title=Changed overrides the playbook's vars:: variables from
-e have the highest precedence. Several — -e 'a=1 b=2' or a file
-e @vars.yml.
The inventory was not read: the file is missing, the path is wrong or
-i was forgotten. The exit code is still 0 — check the path. If the
inventory has no group named in hosts:, you get the same warning with
its name.
-l names a machine or group that is not in the inventory. Compare with
--list-hosts.
Ansible could not connect over SSH: a wrong name or address, no key,
the wrong user. The SSH error text is in the msg field. Check by hand
as the same user: ssh node2 true; details — -vvv. Keys —
ssh-keys.
A typo in the module name, or the collection is missing. The playbook
does not start — code 4. Further down the message gives the file, line
and spot of the error. Module docs — ansible-doc ansible.builtin.service.
notify names a handler that does not exist: the name must match the
handler's name letter for letter, case included. Here notify says
reload nginx, and the handler is called Reload nginx.
A template or task uses a variable that does not exist: a typo, or the
variable is not set in vars:, the inventory or -e. A default value —
{{ site_name | default('Demo') }}.
The indentation does not match: a task key is off by one space. YAML is
strict about indentation, and tabs are not allowed in it. Further down
the message shows the line with ^ under the error; often the line
above it is the culprit.
The task changes something owned by root, and the play runs as a user.
become: true on the play or task means running through sudo;
sudo.
shell or command without creates:/changed_when:, or a template
with a variable that changes every time (a date, a random number). A
handler such a task calls fires every time too.
The playbook goes to every machine in hosts:. Before the first run on
production servers — --list-hosts, --check --diff and -l on one
machine.
shell and command are skipped in check mode: --check shows fewer
changes than a real run makes, and tasks that depend on their result can
fail only in the check.
become: true does.--diff output.Found an inaccuracy? Report an error in the article