skip to content

In Ansible, what do the host patterns `webservers:&staging` and `webservers:!decommissioned` select, and how does `--limit` interact with a play's `hosts:` line?

level: seniorimportance: nice to knowfreq 42%

answer

  1. operators combine group names
  2. one means both, one means minus
  3. the flag filters after the play resolves
  4. it can only ever shrink the set
  5. preview with a dry listing first

basics

~20 s

The first selects hosts in both webservers and staging (intersection); the second selects hosts in webservers that are not in decommissioned (exclusion). --limit narrows what the play's hosts line already matched — it intersects, and can never add hosts the play did not target.

solid answer

~40 s

Patterns compose group and host names with operators: `:` or a comma is union, `:&` is intersection, and `:!` is exclusion, so `webservers:&staging` is hosts in both groups and `webservers:!decommissioned` is the webservers minus anything decommissioned. `--limit` is a separate, later filter: it intersects with whatever the play's `hosts:` already resolved to, so `hosts: webservers --limit db01` runs against nothing and Ansible errors out saying the hosts and limit matched nothing. That one-way property is what makes `--limit` safe as an operational guard rail — it can only ever shrink the target set. In practice I preview with `--list-hosts` before a production run, and remember to quote patterns containing `!` so the shell does not try to expand them.

code

bash · 4 lines
bash
ansible-playbook -i inventories/prod site.yml \
  --limit "webservers:!decommissioned" --list-hosts

ansible -i inventories/prod 'webservers:&staging' --list-hosts

go deeper

for a junior

Know that a play targets a group name or all, and that patterns can combine names — union with a comma, and ! to exclude a group.

for a middle

Explain the operators precisely, including intersection with :&, and that --limit is applied on top of the play's own host selection rather than replacing it.

for a senior

Show the operational discipline: preview with --list-hosts, treat --limit as a guard rail that can only narrow, and keep retired hosts in an excludable group.

for a principal

Own the convention that keeps blast radius bounded — whether broad plays plus a mandatory limit is safer than narrow plays, and how pipelines supply the limit instead of an operator typing it.

## Patterns select from what the inventory produced A pattern is the expression that turns the inventory into the actual host set for a play. It appears in two places: the `hosts:` line of a play, and the first positional argument of the ad-hoc `ansible` command. The building blocks are host names and group names, combined with operators: - `all` or `*` — every host in the inventory. - `web1:web2` or `web1,web2` — union. The comma form is preferred because it does not collide with anything. - `webservers:&staging` — intersection: only hosts that are in *both*. - `webservers:!decommissioned` — exclusion: everything the pattern has accumulated, minus that group. - `~(web|db).*` — a regular expression against host names. - `web[01:20]` — a numeric range over host names, matching the range syntax used when declaring hosts. Intersection is what makes a two-axis inventory work. If hosts are grouped both by role and by environment, then `webservers:&staging` is the only way to say "web servers, but only the staging ones" without maintaining a third group called `staging_webservers` that someone will forget to update. Two mechanical traps. In YAML, a value beginning with certain characters must be quoted, and in a POSIX shell an unquoted `!` can trigger history expansion — so write `hosts: "webservers:!decommissioned"` and quote the argument on the command line. And a pattern that matches nothing is not silently ignored: Ansible tells you the pattern did not match, which is the correct behaviour but surprises people who expected a no-op. ## `--limit` is a second, narrowing filter `--limit` (also `-l`) accepts the same pattern syntax but is applied *after* the play's own `hosts:` line, as an intersection with it. This has one consequence worth stating precisely in an interview: **`--limit` can only shrink the target set, never grow it.** A play declaring `hosts: webservers` and invoked with `--limit db01` runs against nothing, and Ansible stops with an error saying the specified hosts and/or limit did not match any hosts. It does not quietly reach outside the play. That one-way property is exactly why `--limit` is the standard operational guard rail. A team can keep broad plays — `hosts: all` inside a role-driven site playbook — and require every human-initiated run to pass a limit, confident that the flag cannot widen the blast radius by accident. The failure mode is the opposite one: forgetting `--limit` and getting the full play, so pipelines that rely on it should supply it in the job definition rather than trusting an operator to type it. ## Previewing The cheap habit before any production run: ```bash ansible-playbook -i inventories/prod site.yml --limit "webservers:!decommissioned" --list-hosts ansible webservers:'&'staging -i inventories/prod --list-hosts ``` `--list-hosts` resolves the pattern and prints the hosts without connecting to anything. It answers "is this pattern what I think it is" in a second and costs nothing. `--check` is the next step down that road, but `--list-hosts` is the one that catches pattern mistakes, because a wrong pattern in check mode still tells you about the wrong hosts. ## Rolling execution is a different lever Candidates sometimes reach for `--limit` when they mean batching. Narrowing to a subset and running repeatedly is a manual, error-prone way to stage a rollout; the play-level `serial` keyword expresses "this many hosts at a time" declaratively over the full target set. `--limit` answers *which* hosts; `serial` answers *how many at once*. Knowing that they are different questions is part of the answer here. ## What interviewers are really probing This question is a proxy for how you operate. A candidate who reaches for `--list-hosts`, quotes their patterns, knows that `--limit` intersects rather than replaces, and keeps decommissioned hosts in an excludable group is describing a team that does not accidentally reconfigure production. A candidate who thinks `--limit` overrides `hosts:` has just described how a staging run reaches a production database.

  • A play declares `hosts: webservers` and you pass `--limit db01`. What happens?
    Nothing runs. `--limit` intersects with the hosts the play already selected, and `db01` is not among them, so the result is empty and Ansible errors out saying the hosts and/or limit did not match. The flag can never pull in a host the play did not target.
  • Why keep a `decommissioned` group instead of deleting the hosts from the inventory?
    Because an excludable group keeps the record of what existed while removing it from every pattern that uses `:!decommissioned`, and it is reviewable in a diff. Deleting entries loses the history and makes it easy for someone to re-add a host that was retired for a reason.
  • When would you use the play-level `serial` keyword rather than `--limit`?
    When the question is *how many at a time*, not *which hosts*. `serial` runs the full target set in batches so a rolling restart never takes the whole fleet down at once, and it is declared in the play. Using repeated `--limit` invocations to fake batching is manual and easy to get wrong.

saying these in an interview costs you the question

  • Thinking --limit replaces the play's hosts line
  • Believing --limit can add hosts outside the play
  • Assuming a pattern matching nothing is a silent no-op
  • Using repeated --limit runs instead of serial for batching
  • Forgetting to quote patterns containing ! in the shell

context