Skip to main content
RunBook Academy

AnsibleXXX · Host Targeting and Blast RadiusPatterns

How Ansible evaluates a pattern

Intermediate⏱ ~21 minbash

What you'll learn

  • State the three-phase order in which a pattern is evaluated
  • Predict that reordering the terms of a pattern does not change the result
  • Explain why an exclusion cannot be undone by a later union term
  • Design broad-target plays whose exclusion clause is genuinely load-bearing

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.

A pattern with three operators looks like an expression, and expressions usually evaluate left to right. This one does not, and the difference is the reason exclusions can be trusted.

A pattern is evaluated in three phases: every plain term is unioned, then every & term is intersected, then every ! term is removed. The order the terms appear in the string is irrelevant.

That last clause is the surprising half, and it has a direct safety consequence: !do_not_automate protects those hosts no matter where in the pattern it appears and no matter what appears after it.

The demonstration

One target set, six ways of writing the same three terms — web, minus the canary group, intersected with us_east:

Read-only / Safesix permutations, one answer
$ for p in 'web:!canary:&us_east' '&us_east:web:!canary' '!canary:&us_east:web' 'web:&us_east:!canary' '!canary:web:&us_east' '&us_east:!canary:web'; do ansible -i inventory/hosts.yml "$p" --list-hosts; done
web:!canary:&us_east    ->  hosts (3): web02 web03 web04
&us_east:web:!canary    ->  hosts (3): web02 web03 web04
!canary:&us_east:web    ->  hosts (3): web02 web03 web04
web:&us_east:!canary    ->  hosts (3): web02 web03 web04
!canary:web:&us_east    ->  hosts (3): web02 web03 web04
&us_east:!canary:web    ->  hosts (3): web02 web03 web04

Note the third and fifth: a pattern that begins with ! is legal and works. There is no requirement that a positive term come first, because the positive terms are gathered independently of where they sit.

Working the phases by hand on the first form:

Read-only / Safeweb:!canary:&us_east, phase by phase
phase 1, union of plain terms
web            -> web01, web02, web03, web04

phase 2, intersect with every & term
&us_east       -> us_east is web02, web03, web04, db02
                  intersection -> web02, web03, web04

phase 3, remove every ! term
!canary        -> canary is web01
                  web01 is already gone; no change
                  result -> web02, web03, web04

And on &us_east:!canary:web, which reads as though it starts with an intersection against nothing:

Read-only / Safe&us_east:!canary:web, phase by phase
phase 1, union of plain terms
web            -> web01, web02, web03, web04     (the only plain term)

phase 2, intersect
&us_east       -> web02, web03, web04

phase 3, remove
!canary        -> no change

result -> web02, web03, web04     (identical)

The phases are what make them equal. There is no point at which “the current set” is empty because an & came first, because & terms are not applied until every plain term has been collected.

An exclusion cannot be cancelled

This is the property to remember, and it is worth proving rather than asserting.

Read-only / Safenaming the excluded host again does not re-add it
$ ansible -i inventory/hosts.yml 'web:!canary:web01.example.com' --list-hosts
  hosts (3):
  web02.example.com
  web03.example.com
  web04.example.com
Read-only / Safenor does naming the group again
$ ansible -i inventory/hosts.yml 'web:!canary:canary' --list-hosts
  hosts (3):
  web02.example.com
  web03.example.com
  web04.example.com

The corollary: you cannot express “except, but include”

The same property has a cost, and it is worth knowing before you spend twenty minutes trying to write around it.

There is no way, within a single pattern, to say “all the staging hosts except the database ones, but do include stage-db01 specifically”. Once db_stage is excluded, stage-db01 is gone, and adding it back as a union term does nothing.

Three ways out, in order of preference:

  1. Fix the inventory. If you routinely need "db_stage except one host", that host belongs in its own group. A pattern working around inventory structure is a sign the structure is wrong.
  2. Use two plays. One targeting the broad set with the exclusion, one targeting the exception, with the tasks factored into a role so they do not diverge.
  3. Use --limit with an explicit host list, or a limit file. The pattern stays honest and the narrowing happens at invocation time, where a reviewer can see it.

The first is the right answer surprisingly often. A pattern that needs an exception is usually describing a group that the inventory does not have a name for yet, and naming it is both cheaper and more readable than a clever expression nobody can parse six months later.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Do web:!stage and !stage:web produce the same set of hosts?

  2. Q2. An exclusion term stays in effect even if the excluded host is named again later in the same pattern.

  3. Q3. A pattern resolves to zero hosts and produces exactly one warning: "No hosts matched, nothing to do". What does the absence of a second warning tell you?

  4. Q4. You need "all staging hosts except the database ones, but including stage-db01". Which approaches actually work? Select all that apply.

Passing score: 75%. Answers are checked in this browser.