Patterns and Host 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.
# 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.
web:dbweb or db. The colon joins the two sets.web:&prodweb and prod. The & narrows.web:!web1web 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.
# 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.
# 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.
# 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.
# prints the resolved hosts; runs no tasks ansible 'web:&prod' --list-hosts
- Using
web:prod(union) when you meantweb:&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: allplaybook and forgetting--limit, so a change intended for one host reaches the entire inventory before anyone notices the omission. - Trusting a glob like
*.larkspur.iowithout checking--list-hostsfirst, 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.
- Resolve any non-trivial pattern with
--list-hostsbefore running, so the host set is confirmed rather than assumed — especially for set-arithmetic patterns. - Reach for
--limitto 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:&prodover 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--limitfor 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.
Knowledge Check
What is the difference between web:prod and web:&prod?
web:prodis the union (hosts in either group);web:&prodis the intersection (only hosts in both)web:prodis the intersection (hosts in both);web:&prodis the union — the ampersand widens the match- They are identical; the ampersand is just optional syntax kept around for readability
web:prodsubtracts the prod hosts from web;web:&prodadds 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