Skip to main content
RunBook Academy

AnsibleXXX · Host Targeting and Blast RadiusScope from inside the play

ansible_play_hosts, ansible_play_batch and friends

Advanced⏱ ~24 minbash

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

Not yet marked complete on this device.

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.

VariableContains
ansible_play_hosts_allEvery host the play selected, fixed for the whole play
ansible_play_hostsHosts still active — the above, minus failed and unreachable
ansible_play_batchThe 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.

Read-only / Safescope.yml
- 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 }}"
Read-only / Safetwo batches, three variables
$ ansible-playbook -i inventory/hosts.yml scope.yml
PLAY [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:

Read-only / Safeone host drops out
$ ansible-playbook -i inventory/hosts.yml shrink.yml
TASK [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.

Read-only / Safea mid-rollout health gate
- 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 askingThe 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.

Read-only / Safethe two can disagree, legitimately
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_facts

The 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

  1. Q1. A play targets four hosts with serial: 2. During the first batch, what does ansible_play_hosts contain?

  2. Q2. ansible_play_hosts_all shrinks as hosts fail during the play.

  3. Q3. Which variable should a guardrail assert on to refuse a run that is wider than the change ticket authorised?

  4. 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.