AnsibleXXX · Host Targeting and Blast RadiusNarrowing a run
Zero hosts: warning or error
What you'll learn
- Predict the exit code for each of the three zero-host situations
- Explain why a bare pattern and a --limit are treated differently
- Identify the zero-host case that is silent even with --limit
- Make a scheduled or CI run fail when it targets nothing
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
There are three ways to end up targeting no hosts, and they do not all behave the same. Getting this straight is what separates “the job is green because it worked” from “the job is green because nothing happened”.
| Situation | Output | Exit code |
|---|---|---|
The play’s hosts: pattern matches nothing | Warning, skipping: no hosts matched | 0 |
--limit matches nothing in the inventory | [ERROR] ... leaves us with no hosts to target | 1 |
--limit matches a real host outside the play’s hosts: | skipping: no hosts matched | 0 |
Two of those exit 0. One of those two is the reason CI jobs report success for weeks while doing nothing.
Case one: a bare pattern that matches nothing
$ ansible-playbook -i inventory/hosts.yml nohosts.yml; echo exit=$?[WARNING]: Could not match supplied host pattern, ignoring: nosuchgroup
PLAY [Target a group that does not exist] **************************************
skipping: no hosts matched
PLAY RECAP *********************************************************************
exit=0Read the recap. It is empty — not zero-filled, empty. There is no host line to compare against yesterday’s, because there is no host.
A scheduled job in this state produces a log full of nothing and a success every night. Nothing alerts, because nothing failed. The warning is in the log and warnings in a nightly log are not read.
Case two: a --limit that matches nothing
Completely different treatment:
$ ansible-playbook -i inventory/hosts.yml site.yml --limit web01.example.con; echo exit=$?[WARNING]: Could not match supplied host pattern, ignoring: web01.example.con
[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.
exit=1This is the behaviour you want, and it is the strongest single argument
for making --limit the habitual way to narrow a run: a typo in a
--limit fails closed. Nothing happened, the exit code says so, and
the scheduler notices.
Case three: the silent one
Now the combination that is neither of the above. The limit is a real host. It is simply not in the play’s target.
$ ansible-playbook -i inventory/hosts.yml site.yml --limit db01.example.com; echo exit=$?PLAY [Configure the web tier] **************************************************
skipping: no hosts matched
PLAY RECAP *********************************************************************
exit=0There is not even a warning here. Every term resolved. The set arithmetic produced nothing, which Ansible has no reason to consider remarkable.
Why the asymmetry exists
It is not arbitrary, and understanding it makes the behaviour predictable rather than something to memorise.
A play’s hosts: line is a statement of intent. “Run this on the
web servers” is a meaningful instruction in an estate that has no web
servers today — the play is correct, there is just nothing to do. A
site.yml containing twenty plays, run in an environment where six of
those tiers do not exist, should run the fourteen that do rather than
refusing. That is only possible if an empty play is survivable.
A --limit is a narrowing of something you have already decided to
run. Supplying a limit that matches no host in the inventory is not a
description of an empty set; it is almost always a typo. Ansible treats
it as one.
Case three sits between the two: the limit resolved, so it was not a typo; the play’s pattern resolved, so it was not wrong either; the intersection was empty, and neither party is at fault. Nothing in the model has an opinion about that, so nothing complains.
Making zero hosts fail
Two layers, because neither catches everything.
Layer one — inside the play — catches “fewer than expected”, not zero. An assertion in a pre-flight play refuses an implausible count:
- name: Pre-flight - the target must be plausible
hosts: web
gather_facts: false
vars:
expected_min_hosts: 2
tasks:
- name: Refuse an implausibly small target
run_once: true
ansible.builtin.assert:
that:
- ansible_play_hosts_all | length >= expected_min_hosts | int
fail_msg: >-
Targeting {{ ansible_play_hosts_all | length }} hosts, expected at
least {{ expected_min_hosts }}.It cannot catch zero. With no hosts in the play there is nothing for the
task to run on, so the assertion never executes and the play reports
skipping: no hosts matched exactly as before. That is not a flaw in
the assertion — it is a structural consequence of tasks running on
hosts.
Layer two — outside the play — is what catches zero. Resolve the
pattern first, read the count, and refuse before ansible-playbook is
invoked at all:
#!/usr/bin/env bash
set -euo pipefail
PATTERN=${1:?usage: run-change.sh <pattern>}
FLOOR=${FLOOR:-1}
count=$(ansible -i inventory/ "$PATTERN" --list-hosts | sed -n 's/^ *hosts (\([0-9]*\)):.*/\1/p')
if [ -z "$count" ] || [ "$count" -lt "$FLOOR" ]; then
echo "refusing: pattern '$PATTERN' resolved to ${count:-0} hosts, floor is $FLOOR" >&2
exit 1
fi
echo "$PATTERN resolves to $count hosts; proceeding"
ansible-playbook -i inventory/ site.yml --limit "$PATTERN"$ run-change.sh nosuchgroup; FLOOR=20 run-change.sh web[WARNING]: Could not match supplied host pattern, ignoring: nosuchgroup
[WARNING]: No hosts matched, nothing to do
refusing: pattern 'nosuchgroup' resolved to 0 hosts, floor is 1
rc=1
refusing: pattern 'web' resolved to 4 hosts, floor is 20
rc=1- For anything scheduled, wrap it. The wrapper resolves the pattern, reads the count, and exits non-zero below the floor - which is the only layer that catches zero hosts.
- Set the floor from a snapshot rather than a guess. "At least one" catches the typo; "at least 90% of last week" catches the partial inventory.
- Add the in-play assertion as well, because it catches the case the wrapper cannot: a per-play intersection that empties in a multi-play site.yml where the overall count looked fine.
- Alert on a run whose changed count has been zero for longer than usual. A patching job that changes nothing three weeks running is either perfectly converged or not running at all, and those need to be distinguishable.
- Prefer --limit over editing the hosts: line for one-off narrowing, because a typo in a limit is an error and a typo in a pattern is a warning.
Knowledge check
Knowledge check · 4 questions
Q1. A nightly job has reported success for three weeks and changed nothing. Investigation shows the group named in the play hosts: line was renamed. What did the job do each night?
Q2. Why is an unmatched --limit a hard error when an unmatched play pattern is not?
Q3. Which situations produce zero hosts and exit code 0? Select all that apply.
Q4. An assert task inside the play can be used to fail a run that targeted zero hosts.
Passing score: 75%. Answers are checked in this browser.