Skip to main content
RunBook Academy

AnsibleXXX · Host Targeting and Blast RadiusPatterns

Host pattern syntax

Intermediate⏱ ~28 minbash

What you'll learn

  • Write the correct pattern for a stated target set using union, intersection and exclusion
  • Predict what a glob matches, including the group names it matches
  • Use group indexing and slices, and state which slice forms are supported
  • Recognise the pattern forms that look plausible and are not supported

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.

Pattern syntax is the most confidently misremembered part of Ansible. People are sure web[:3] works, sure that a slice is exclusive at the upper bound like Python’s, and sure that an IP glob matches by address. None of those is true in ansible-core 2.21.

So everything below was run. The inventory is ten hosts in overlapping groups, using documentation-reserved addresses, and every output is the real thing:

Read-only / Safeinventory/hosts.yml - the inventory every example uses
all:
children:
  production:
    children:
      web:
        hosts:
          web01.example.com: {ansible_host: 192.0.2.11}
          web02.example.com: {ansible_host: 192.0.2.12}
          web03.example.com: {ansible_host: 192.0.2.13}
          web04.example.com: {ansible_host: 192.0.2.14}
      db:
        hosts:
          db01.example.com: {ansible_host: 192.0.2.21}
          db02.example.com: {ansible_host: 192.0.2.22}
      cache:
        hosts:
          cache01.example.com: {ansible_host: 192.0.2.31}
  staging:
    children:
      web_stage:
        hosts:
          stage-web01.example.com: {ansible_host: 198.51.100.11}
          stage-web02.example.com: {ansible_host: 198.51.100.12}
      db_stage:
        hosts:
          stage-db01.example.com: {ansible_host: 198.51.100.21}
  eu_west:
    hosts: {web01.example.com: , db01.example.com: , cache01.example.com: }
  us_east:
    hosts: {web02.example.com: , web03.example.com: , web04.example.com: , db02.example.com: }
  canary:
    hosts: {web01.example.com: }
  do_not_automate:
    hosts: {db01.example.com: }

Verify anything here yourself with ansible -i inventory/hosts.yml '<pattern>' --list-hosts, which connects to nothing and is the safest command in the course.

The complete vocabulary

FormMeaningExample
all or *Every host in the inventoryall
nameOne host, or one groupweb01.example.com, web
: or ,Union — addweb:db
:&Intersection — andweb:&eu_west
:!Exclusion — removeweb:!canary
* inside a nameGlob, over host and group namesweb*
~ prefixRegular expression~web0[13]
name[n]The nth member of a group, zero-basedweb[0]
name[n:m]Members n to m inclusiveweb[0:2]
name[n:]From n to the endweb[1:]
name[-1]The last memberweb[-1]

, and : are interchangeable for union. Prefer , when the pattern contains an IPv6 address, since : is ambiguous there; prefer : otherwise for consistency with :& and :!.

Union, intersection, exclusion

The three operators, each against the same inventory.

Read-only / Safeunion - everything in either group
$ ansible -i inventory/hosts.yml 'web:db' --list-hosts
  hosts (6):
  web01.example.com
  web02.example.com
  web03.example.com
  web04.example.com
  db01.example.com
  db02.example.com
Read-only / Safeintersection - in both groups
$ ansible -i inventory/hosts.yml 'web:&eu_west' --list-hosts
  hosts (1):
  web01.example.com
Read-only / Safeexclusion - everything except
$ ansible -i inventory/hosts.yml 'web:!canary' --list-hosts
  hosts (3):
  web02.example.com
  web03.example.com
  web04.example.com

They combine, and the result does not depend on where in the string each term appears:

Read-only / Safeall three at once
$ ansible -i inventory/hosts.yml 'web:!canary:&us_east' --list-hosts
  hosts (3):
  web02.example.com
  web03.example.com
  web04.example.com

Globs match group names too

This is the trap that catches experienced people, and it is entirely logical once seen.

Read-only / Safeweb* reaches staging
$ ansible -i inventory/hosts.yml 'web*' --list-hosts
  hosts (6):
  web01.example.com
  web02.example.com
  web03.example.com
  web04.example.com
  stage-web01.example.com
  stage-web02.example.com

A pattern is matched against every host name and every group name in the inventory. web* matched the four web*.example.com hosts by name and the web_stage group by name, and a group match contributes all its members.

The operational consequence is direct: a glob you wrote to mean “the web servers” reached staging, and would equally reach production if the groups were named the other way round. Group naming conventions and glob patterns interact, and the interaction is invisible until you print the host list.

Regular expressions

Prefix with ~ and the rest is a Python regular expression:

Read-only / Safea regex over host names
$ ansible -i inventory/hosts.yml '~web0[13]' --list-hosts
  hosts (2):
  web01.example.com
  web03.example.com

Note what that implies about anchoring. ~web0[13] matched web01.example.com, which the expression does not describe in full — so the match is applied at the start of the name and does not have to consume all of it. The mirror test confirms it:

Read-only / Safea regex that only matches in the middle
$ ansible -i inventory/hosts.yml '~example' --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: ~example
[WARNING]: No hosts matched, nothing to do
hosts (0):
Read-only / Safean explicit start anchor behaves identically
$ ansible -i inventory/hosts.yml '~^web' --list-hosts
  hosts (6):
  web01.example.com
  web02.example.com
  web03.example.com
  web04.example.com
  stage-web01.example.com
  stage-web02.example.com

Indexing and slicing a group

Useful for canaries: “the first host of this group”, without hardcoding which host that is.

Read-only / Safeone member by index
$ ansible -i inventory/hosts.yml 'web[0]' --list-hosts
  hosts (1):
  web01.example.com
Read-only / Safea slice - and the count is the surprise
$ ansible -i inventory/hosts.yml 'web[0:2]' --list-hosts
  hosts (3):
  web01.example.com
  web02.example.com
  web03.example.com

An open-ended upper bound works, and behaves as you would expect:

Read-only / Safefrom an index to the end
$ ansible -i inventory/hosts.yml 'web[1:]' --list-hosts
  hosts (3):
  web02.example.com
  web03.example.com
  web04.example.com
Read-only / Safea negative index
$ ansible -i inventory/hosts.yml 'web[-1]' --list-hosts
  hosts (1):
  web04.example.com

The forms that are not supported

Verified, because “it looked like it should work” is how these get into production scripts.

Read-only / Safean omitted lower bound is not a pattern
$ ansible -i inventory/hosts.yml 'web[:3]' --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: web[:3]
[WARNING]: No hosts matched, nothing to do
hosts (0):
Read-only / Safenegative slices are not supported either
$ ansible -i inventory/hosts.yml 'web[-2:]' --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: web[-2:]
[WARNING]: No hosts matched, nothing to do
hosts (0):
Read-only / Safepatterns match names, not addresses
$ ansible -i inventory/hosts.yml '192.0.2.*' --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: 192.0.2.*
[WARNING]: No hosts matched, nothing to do
hosts (0):
Read-only / Safematching is case sensitive
$ ansible -i inventory/hosts.yml 'WEB' --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: WEB
[WARNING]: No hosts matched, nothing to do
hosts (0):

Knowledge check

Knowledge check · 5 questions

  1. Q1. A group web contains web01 through web04 in that order. How many hosts does the pattern web[0:2] resolve to?

  2. Q2. An inventory has a group web with four hosts and a group web_stage with two staging hosts. The pattern web* resolves to how many hosts, and why?

  3. Q3. Which of these patterns resolve to zero hosts in ansible-core 2.21? Select all that apply.

  4. Q4. A typo in an exclusion term makes the run target fewer hosts than intended.

  5. Q5. A shared playbook uses hosts: "{{ target_group | default('all') }}" so it is convenient to run. What is wrong with that?

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