Chapter 2: Inventory
Topic 10

Patterns and Host Targeting

Targeting

A pattern is how you tell Ansible which hosts a run applies to — the web in ansible web -m ping, or the hosts: line at the top of a play. It is the bridge between the inventory model and the action, the part that decides who gets touched.

Patterns range from a single host name to set arithmetic across groups, and understanding them is what stops a "quick fix on one host" from quietly hitting the whole fleet. The difference between a union and an intersection is one character, and that one character is the difference between configuring three hosts and configuring thirty.

Basic Patterns

The everyday pattern is a group name. ansible web -m ansible.builtin.ping targets every host in web; a hostname like web1.larkspur.io targets exactly that machine; all or * targets everything in the inventory. Most runs never need more than a group name.

On top of whatever a play's hosts: line says, --limit narrows the run to a subset at execution time without editing the play. The command below pings the web tier, then the same intent restricted to one host with a limit.

A group pattern, then narrowed with --limit
# every host in the web group
ansible web -m ansible.builtin.ping

# the same, restricted to one host at run time
ansible web --limit web1.larkspur.io -m ansible.builtin.ping

Set Operations

Patterns compose with set arithmetic. web:db is the union — hosts in either group. web:&prod is the intersection — only the web hosts that are also in prod. web:!web1.larkspur.io is exclusion — the web tier with that one host removed. The colon joins, :& intersects, :! subtracts.

Set arithmetic — one character changes the blast radius
web:db
Union — every host in web or db. The colon joins the two sets.
web:&prod
Intersection — only the hosts in web and prod. The & narrows.
web:!web1
Exclusion — the web tier with web1.larkspur.io removed. The ! subtracts.

Intersection and exclusion are how you say precisely what you mean: "the web tier in production, except the one box I am debugging right now." That target is web:&prod:!web1.larkspur.io, and spelling it out is far safer than eyeballing a group and hoping it does not include something it should not.

Union, intersection, and exclusion in one pattern
# web hosts that are also prod, minus the one being debugged
ansible 'web:&prod:!web1.larkspur.io' -m ansible.builtin.ping

Wildcards and Ranges

A glob matches by name. *.larkspur.io catches every host whose name ends that way, which is convenient and blunt in equal measure — a glob can catch more than you remember existing. A positional range like web[0:1] slices the matched hosts by index rather than by name, taking the first two.

Globs earn their keep when names encode structure, but they reward verification. The pattern below matches all of Larkspur by name suffix; whether that is three hosts or thirty depends entirely on what the inventory currently holds, which is the next section's whole point.

A name glob matches by suffix — convenient but blunt
# every host whose name ends in .larkspur.io
ansible '*.larkspur.io' -m ansible.builtin.ping

--limit as a Safety Rail

--limit restricts a playbook to a subset regardless of what the play's hosts: line says. A play written as hosts: web still runs against only web1.larkspur.io when invoked with --limit web1.larkspur.io — the flag intersects with the play's target rather than replacing it.

That makes it the standard way to canary a change. Run the playbook against one host, confirm it does what you expect, then run it again without the limit to let it reach the full group. The play never changes; the blast radius is controlled entirely from the command line, which is where you want that decision.

Canary a playbook on one host before widening
# play targets hosts: web, but this run touches only web1
ansible-playbook site.yml --limit web1.larkspur.io

# confirmed — now let it run against the whole web group
ansible-playbook site.yml

Verifying Before You Run

ansible <pattern> --list-hosts prints exactly which hosts a pattern resolves to and does nothing else — no connection, no task, just the resolved set. It is the check that turns a union/intersection guess into a confirmed list before any change goes out.

Run it on anything non-trivial, especially the set-arithmetic patterns where one misplaced character flips a union into an intersection. The command below prints the production web hosts a pattern resolves to; seeing the list is what keeps the mistake from becoming an outage.

--list-hosts confirms the target without touching anything
# prints the resolved hosts; runs no tasks
ansible 'web:&prod' --list-hosts
Common Mistakes
  • Using web:prod (union) when you meant web:&prod (intersection), so a play hits every web host and every prod host instead of just the prod web hosts — one missing ampersand, a much larger blast radius.
  • Running a hosts: all playbook and forgetting --limit, so a change intended for one host reaches the entire inventory before anyone notices the omission.
  • Trusting a glob like *.larkspur.io without checking --list-hosts first, then catching a host you forgot existed and configuring it along with the rest.
  • Excluding a host with ! in one run and assuming the exclusion sticks — exclusions are per-invocation, so the next run without the flag silently includes it again.
  • Editing a play's hosts: line down to a single host to test on it, then forgetting to widen it back, so the real run only ever touches the canary.
Best Practices
  • Resolve any non-trivial pattern with --list-hosts before running, so the host set is confirmed rather than assumed — especially for set-arithmetic patterns.
  • Reach for --limit to canary a change on one host before widening to the group, particularly for anything touching production where a mistake is expensive.
  • Prefer explicit intersections like web:&prod over relying on a glob to land on the right hosts, so the target is exactly the set you mean and nothing adjacent.
  • Keep hosts: in plays at the natural group level and use --limit for run-time narrowing, rather than editing the play to test on a subset and risking forgetting to revert.
  • Treat an exclusion as a one-time flag, not a permanent setting — if a host should always be skipped, change the inventory or grouping, not a single invocation.
Comparable tools salt -C compound matchers the Salt analog for set-style host targeting Shell globbing over a hosts file the unstructured version of the same idea Terraform no equivalent — it targets resources, not hosts

Knowledge Check

What is the difference between web:prod and web:&prod?

  • web:prod is the union (hosts in either group); web:&prod is the intersection (only hosts in both)
  • web:prod is the intersection (hosts in both); web:&prod is the union — the ampersand widens the match
  • They are identical; the ampersand is just optional syntax kept around for readability
  • web:prod subtracts the prod hosts from web; web:&prod adds the prod hosts to web

What does --limit protect against?

  • A change reaching more hosts than intended — it restricts a run to a subset regardless of the play's hosts: line, so you canary first
  • A play failing when one host is unreachable, by silently skipping any host in the targeted set that does not respond within the connection timeout
  • Variables leaking between groups, by isolating each group's variable scope so one group cannot read another's
  • Slow runs, by capping how many of the targeted hosts Ansible contacts in parallel at any one time

Why should --list-hosts precede a risky run?

  • It prints exactly which hosts a pattern resolves to without doing anything, turning a set-arithmetic guess into a confirmed list
  • It runs the whole play inside a sandbox first, so any mistake it makes is rolled back automatically
  • It locks the inventory for the duration so no other run can target the same hosts at the same time
  • It is mandatory before any playbook; Ansible flatly refuses to run a host pattern that has not first been listed with this exact flag

Does excluding a host with ! persist across runs?

  • No — exclusions are per-invocation, so the next run without the flag includes the host again
  • Yes — Ansible records the exclusion in its state file and applies it to every later run automatically
  • Yes — once excluded with !, a host is removed from the inventory and must be re-added to be reached
  • It persists only until the control node next reboots, and then quietly resets itself

You got correct