AnsibleXXX · Host Targeting and Blast RadiusScope from inside the play
ansible_play_hosts, ansible_play_batch and friends
What you'll learn
- Distinguish ansible_play_hosts, ansible_play_batch and ansible_play_hosts_all
- Predict what each contains under serial and after a host fails
- Choose the right variable for a host-count assertion
- Separate inventory_hostname from ansible_hostname and know which is reliable
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Everything so far has verified scope from outside the play, before it runs. This lesson is the inside view: how a play answers “how big am I” about itself, while executing.
That matters because a guardrail written in a playbook travels with the playbook. A wrapper script protects the runs that go through the wrapper; an assertion inside the play protects every run, including the one somebody types by hand at two in the morning.
Three variables carry the answer, and they are close enough in name that picking the wrong one produces a guardrail that quietly never fires.
| Variable | Contains |
|---|---|
ansible_play_hosts_all | Every host the play selected, fixed for the whole play |
ansible_play_hosts | Hosts still active — the above, minus failed and unreachable |
ansible_play_batch | The hosts in the current serial batch |
Under serial, verified
The common misreading is that ansible_play_hosts narrows to the
current batch. It does not.
- name: Show the scope variables
hosts: web
gather_facts: false
serial: 2
tasks:
- name: Report scope
run_once: true
ansible.builtin.debug:
msg:
- "play_hosts_all={{ ansible_play_hosts_all }}"
- "play_hosts={{ ansible_play_hosts }}"
- "play_batch={{ ansible_play_batch }}"$ ansible-playbook -i inventory/hosts.yml scope.ymlPLAY [Show the scope variables] ************************************************
TASK [Report scope] ************************************************************
ok: [web01.example.com] => {
"msg": [
"play_hosts_all=['web01.example.com', 'web02.example.com', 'web03.example.com', 'web04.example.com']",
"play_hosts=['web01.example.com', 'web02.example.com', 'web03.example.com', 'web04.example.com']",
"play_batch=['web01.example.com', 'web02.example.com']"
]
}
PLAY [Show the scope variables] ************************************************
TASK [Report scope] ************************************************************
ok: [web03.example.com] => {
"msg": [
"play_hosts_all=['web01.example.com', 'web02.example.com', 'web03.example.com', 'web04.example.com']",
"play_hosts=['web01.example.com', 'web02.example.com', 'web03.example.com', 'web04.example.com']",
"play_batch=['web03.example.com', 'web04.example.com']"
]
}Note also that the PLAY header appears twice. Under serial, each
batch is its own execution of the play — which is why handlers flush per
batch and why a failure in batch one stops batch two from starting.
After a failure, verified
ansible_play_hosts is the one that changes during the play. Here a
mid-play task fails on one host:
$ ansible-playbook -i inventory/hosts.yml shrink.ymlTASK [Report before] ***********************************************************
ok: [web01.example.com] => {
"msg": "before: play_hosts=4 play_hosts_all=4"
}
TASK [Fail on web02] ***********************************************************
fatal: [web02.example.com]: FAILED! => {
"assertion": "inventory_hostname != 'web02.example.com'",
"msg": "deliberate"
}
TASK [Report after] ************************************************************
ok: [web01.example.com] => {
"msg": "after: play_hosts=['web01.example.com', 'web03.example.com', 'web04.example.com'] play_hosts_all=4"
}That is what “still active” means, and it makes ansible_play_hosts
the right variable for a different question: how much of the fleet is
still healthy partway through a rollout.
- name: Stop if too much of the fleet has dropped out
run_once: true
ansible.builtin.assert:
that:
- (ansible_play_hosts | length) >= (ansible_play_hosts_all | length * 0.8)
fail_msg: >-
Only {{ ansible_play_hosts | length }} of
{{ ansible_play_hosts_all | length }} hosts are still active.
Halting before the next phase.Choosing the right one
| The question you are asking | The variable |
|---|---|
| How many hosts can this run possibly affect? | ansible_play_hosts_all |
| Is this run wider than the change ticket says? | ansible_play_hosts_all |
| How much of the fleet is still healthy right now? | ansible_play_hosts |
| Have too many hosts dropped out to continue? | ansible_play_hosts |
| Is this batch too large, or does it hold two quorum members? | ansible_play_batch |
| Which host am I? | inventory_hostname |
inventory_hostname is not ansible_hostname
A related pair, and confusing them produces bugs that only appear on some hosts.
inventory_hostname is the name this host has in the inventory.
It always exists, it never requires a connection, and it is what
patterns, group_vars filenames and --limit all match against.
ansible_hostname is a gathered fact — the short hostname the
machine reports about itself. It requires gather_facts: true and a
successful connection, and it can differ from the inventory name for
entirely ordinary reasons: the inventory uses an FQDN and the host is
configured with a short name, the inventory uses an alias with
ansible_host pointing elsewhere, the machine was rebuilt and renamed.
inventory entry: web01.example.com
ansible_host: 192.0.2.11
inventory_hostname web01.example.com always available
ansible_hostname web01 requires gather_facts
ansible_fqdn web01.example.com requires gather_factsThe rule that avoids the whole class of bug: use
inventory_hostname for anything to do with identity, targeting,
grouping or file naming, and ansible_hostname only when you genuinely
need what the machine calls itself — writing its own hostname into a
config file it serves, for instance.
A conditional written as when: ansible_hostname == 'web01' breaks
silently on any host whose configured name differs from its inventory
name, and works everywhere you tested. inventory_hostname has no such
failure mode, and works with gather_facts: false.
Knowledge check
Knowledge check · 4 questions
Q1. A play targets four hosts with serial: 2. During the first batch, what does ansible_play_hosts contain?
Q2. ansible_play_hosts_all shrinks as hosts fail during the play.
Q3. Which variable should a guardrail assert on to refuse a run that is wider than the change ticket authorised?
Q4. Why should a conditional use inventory_hostname rather than ansible_hostname? Select all that apply.
Passing score: 75%. Answers are checked in this browser.