AnsibleIV · Inventory FundamentalsInventory mechanics
Ranges, aliases and connection variables
What you'll learn
- Write numeric, stride and alphabetic host ranges and predict exactly what they expand to
- Separate a host alias from the address it connects to using ansible_host
- Place ansible_host, ansible_port, ansible_user and ansible_connection in inventory and justify it
- Recognise the blast-radius hazard of a range that expands to more hosts than exist
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
Two mechanisms in this lesson, and they solve opposite problems.
Ranges compress a regular fleet into one line. Aliases and connection variables handle the irregular hosts that a range cannot describe — the one on a non-standard port, the one behind a bastion, the one whose name in your head is not the name DNS knows.
Between them they cover almost everything a static inventory needs to say about how to reach a host, as distinct from what to do to it.
Ranges
Three forms, and all three are worth knowing because you will meet all three in inherited repositories.
[webservers]
web[01:04].example.com
[oddweb]
node[01:07:2].example.com
[shards]
db-[a:d].example.com
$ ansible-inventory -i inventory/hosts.ini --graph@all:
|--@ungrouped:
|--@webservers:
| |--web01.example.com
| |--web02.example.com
| |--web03.example.com
| |--web04.example.com
|--@oddweb:
| |--node01.example.com
| |--node03.example.com
| |--node05.example.com
| |--node07.example.com
|--@shards:
| |--db-a.example.com
| |--db-b.example.com
| |--db-c.example.com
| |--db-d.example.comThe rules the output demonstrates:
[01:04]is inclusive at both ends. Four hosts, not three. This differs from most programming languages you know, and it is the single commonest range mistake.- Leading zeros are preserved.
[01:04]givesweb01;[1:4]givesweb1. The width of the first number sets the padding, so the hostnames match whatever your naming convention actually is. [01:07:2]is start, end, stride. It produced the odd numbers from 1 to 7. This form is rare and almost always a sign that the inventory is encoding something — alternating racks, odd and even power feeds — that would be clearer as two explicit groups.[a:d]walks the alphabet, also inclusive.
Aliases: the name and the address are different things
An inventory host name does not have to resolve. You can name a host whatever is useful and tell Ansible separately where to connect:
[edge]
edge1 ansible_host=192.0.2.11
edge2 ansible_host=192.0.2.12
$ ansible-inventory -i inventory/hosts.ini --graph edge --vars@edge:
|--edge1
| |--{ansible_host = 192.0.2.11}
|--edge2
| |--{ansible_host = 192.0.2.12}The alias — edge1 — is what appears in the recap, in --limit, in
group_vars filenames and in log output. ansible_host is where the
SSH connection goes. They are entirely independent.
This is genuinely useful for:
- Hosts with no DNS, or with DNS that is wrong, or that is right only from inside a network you are not in.
- Naming by role rather than by hostname, so the recap reads
db-primaryinstead ofpgsql-prod-euw1-0447a.internal. - Two entries for the same machine reached by different paths — one via a bastion, one direct — which is occasionally the honest way to express a dual-homed host.
It is also a way to make an inventory lie convincingly, which is why the alias should be more meaningful than the hostname, never less.
The connection variables
Four variables carry most of the “how do I reach this” burden.
ansible_host: ansible_host: 203.0.113.9
ansible_port: ansible_port: 2222
ansible_user: ansible_user: ops
ansible_connection: ansible_connection: local01
ansible_host= ansible_host: 203.0.113.9The address or name to connect to, overriding the inventory host name.
Production: Set this when the inventory name is an alias, or when DNS is not authoritative from the controller. It is the only one of these four that changes which machine you reach.
⚠ Confusing it with ansible_hostname, which is a gathered fact reporting what the host calls itself. Setting ansible_hostname in inventory does not redirect anything.
02
ansible_port= ansible_port: 2222The SSH port. Defaults to 22.
Production: A per-host fact about the estate, so it belongs beside the host. An estate with a handful of non-standard ports will otherwise grow a conditional in every playbook.
⚠ Setting it as a group variable on a group that only mostly uses that port. One member on 22 then fails, and the failure looks like a network problem.
03
ansible_user= ansible_user: opsThe remote user to log in as. Defaults to the controller user running the command.
Production: Set it explicitly rather than relying on the default, so a run behaves identically for every operator and for the scheduled job. Group level is usually right - the account is a property of how the estate was built.
⚠ Leaving it unset on a shared controller. The playbook then connects as whoever ran it, which works interactively and fails in cron under a service account.
04
ansible_connection= ansible_connection: localWhich connection plugin to use. Defaults to ssh; local, docker, podman and network plugins are the common alternatives.
Production: Set it on the group whose hosts genuinely need a different transport - containers in a test scenario, or the controller itself. Never as a fleet-wide default.
⚠ Setting ansible_connection: local on a host you meant to reach over SSH. Every task then executes on the controller against the controller, reports success, and changes the wrong machine.
A host with all four looks like this, and it is the shape of every awkward machine in an estate:
all:
children:
legacy:
hosts:
oldbox:
ansible_host: 192.0.2.55
ansible_user: root
ansible_python_interpreter: /usr/bin/python3.9
$ ansible-inventory -i inventory/hosts.yml --graph legacy --vars@legacy:
|--oldbox
| |--{ansible_host = 192.0.2.55}
| |--{ansible_python_interpreter = /usr/bin/python3.9}
| |--{ansible_user = root}Why these belong in inventory
The alternative is setting them in the play, and the argument against that is worth making explicitly because the play is where people naturally reach for.
They describe the host, not the change. ansible_port: 2222 is true
of that machine regardless of what you are doing to it. Putting it in a
play means every play that touches the host must repeat it, and a play
that forgets fails in a way that looks like a network fault.
They must be right for ad-hoc commands too. ansible legacy -m ping
has no play to read settings from. If the connection details are not in
inventory, the ad-hoc command — which is what you reach for during an
incident — does not work.
They are reviewed with the host. A change to how you reach a machine appears in the inventory diff, next to the machine, where the person reviewing knows what it means.
The general principle, which the course returns to: inventory answers which hosts and how to reach them; playbooks answer what to do to them. Connection variables are unambiguously the first kind.
Knowledge check
Knowledge check · 4 questions
Q1. How many hosts does `node[01:07:2].example.com` create?
Q2. A range that expands to more hosts than actually exist is more dangerous than one that expands to fewer.
Q3. Why do connection variables belong in inventory rather than in a play? Select all that apply.
Q4. A play reports a clean run against 12 hosts in a group, but none of the 12 machines changed. Files were modified on the controller instead. What should you check?
Passing score: 75%. Answers are checked in this browser.