2. Inventory
Ansible · 2. InventoryEverything in Ansible starts with a question: which machines? The inventory is the answer, and it is a more interesting object than a list of hostnames.
The simplest inventory
Section titled “The simplest inventory”[webservers]web1.example.comweb2.example.comweb3.example.com
[databases]db1.example.comdb2.example.com
[production:children]webserversdatabasesThree groups, one of which contains the other two. Now:
ansible webservers -m pingansible production -m pingansible '!databases' -m ping # everything exceptansible 'web*' -m ping # globThe YAML form is more verbose and better for anything real, because it nests:
all: children: production: children: webservers: hosts: web1.example.com: web2.example.com: web3.example.com: databases: hosts: db1.example.com: postgres_role: primary db2.example.com: postgres_role: replicaGroups are the point
Section titled “Groups are the point”The instinct is to think of inventory as a list of hosts. It is more useful to think of it as a set of labels applied to machines, because that is what you write playbooks against.
A host is normally in SEVERAL groups at once:
web1.example.com ├─ webservers (what it does) ├─ production (which environment) ├─ london (where it is) ├─ ubuntu_22 (what it runs) └─ pci_scope (which rules apply to it)Each of those groups can carry its own variables, and a host inherits from all of them. That is what makes inventory expressive: you do not write “for web1 use these 40 settings”, you write “production servers use TLS”, “london servers use this NTP pool”, “PCI-scope servers have this audit rule”, and web1 gets all three because of what it is.
Two groups always exist, whether you define them or not:
all every host in the inventory ungrouped hosts that are in no other group — a useful thing to alert onWhere variables live
Section titled “Where variables live”You can put variables directly in the inventory file, and for anything beyond a couple of values you should not. The convention is directories next to the inventory:
inventory/ hosts.yml group_vars/ all.yml ← every host webservers.yml production.yml production/ ← or a DIRECTORY of files, vault.yml all merged together tuning.yml host_vars/ web1.example.com.yml db1.example.com.ymlAnsible loads these automatically by name. group_vars/webservers.yml applies to every host in webservers, with no reference to it anywhere.
A group named after the file is not required to exist, which is a common source of silent nothing-happening: group_vars/webserver.yml (singular) next to a group called webservers (plural) loads for nobody and produces no error. Chapter 14 lists this among the failures that waste an afternoon.
The precedence between overlapping groups is chapter 6’s subject, and the one rule to carry now: all is the weakest, a child group beats its parent, and host_vars beats every group.
Patterns
Section titled “Patterns”The host pattern is the first argument to ansible and the hosts: line of a play. It is more capable than most people use.
all or * everything webservers one group web1.example.com one host webservers:databases union (OR) webservers:&london intersection (AND) webservers:!web3 exclusion web*.example.com glob ~web\d+\.example\.com regex (leading tilde) webservers[0] the first host in the group webservers[0:2] a slice — the first threeThe intersection and slice forms are the useful ones. webservers:&london is “web servers in London, and nothing else”. webservers[0] is how you run something once on a canary before the rest.
And the flag that belongs in your fingers:
ansible-playbook site.yml --limit web1.example.comansible-playbook site.yml --limit 'webservers:&london'--limit narrows whatever the playbook says. It is the difference between “run this play on production” and “run this play on the one host I am currently fixing”, and it is the single most important safety flag in the tool.
Checking what you have
Section titled “Checking what you have”Never guess what the inventory resolves to. Three commands:
ansible-inventory --list # everything, as JSONansible-inventory --graph # the treeansible-inventory --graph --vars # with variablesansible-inventory --host web1.example.com # one host's full vars
ansible webservers --list-hosts # just: who matches?$ ansible-inventory --graph@all: |--@production: | |--@webservers: | | |--web1.example.com | | |--web2.example.com | |--@databases: | | |--db1.example.com |--@ungrouped:ansible-inventory --graph before a first run on a new inventory answers the two questions that actually matter — is everything in the group I think, and is anything sitting in ungrouped because of a typo.
Dynamic inventory
Section titled “Dynamic inventory”Writing hostnames down stops working the moment machines are created and destroyed by something else. Then the inventory becomes a plugin that asks the source of truth.
# inventory/aws_ec2.yml — the filename suffix mattersplugin: amazon.aws.aws_ec2regions: - eu-west-1filters: tag:Environment: production instance-state-name: runningkeyed_groups: - key: tags.Role # tag Role=web → group "web" prefix: '' separator: '' - key: placement.availability_zone prefix: azhostnames: - private-ip-addresscompose: ansible_host: private_ip_addressNow ansible webservers means “every running EC2 instance tagged Role=web in production, right now”. Nobody maintains a list, and a machine that was terminated an hour ago is simply not there.
The plugins that matter:
amazon.aws.aws_ec2 EC2 instances + their tags google.cloud.gcp_compute azure.azcollection.azure_rm community.general.proxmox community.docker.docker_containers kubernetes.core.k8s pods as hosts community.general.consul Consul's service catalogue ansible.builtin.constructed build groups from existing factsThe Consul one is worth noticing, because it closes a loop that appears repeatedly across this collection: the same registry that Prometheus scrapes from and that HAProxy builds its backend list from can also be the thing that tells Ansible which machines exist. One source of truth for “what is running” is worth a great deal, and it is the argument the Consul guide makes from the other end.
The two rules of dynamic inventory
Section titled “The two rules of dynamic inventory”keyed_groups is where the value is. Anyone can get a list of instances. The point is turning cloud metadata into the groups your playbooks are written against, so that tagging an instance Role=web is all it takes to make it a web server. Get the tagging convention right and inventory maintenance disappears entirely.
Cache it. Every playbook run otherwise makes API calls to your cloud provider, which is slow and eventually rate-limited.
# ansible.cfg[inventory]cache = truecache_plugin = jsonfilecache_connection = ~/.ansible/inventory_cachecache_timeout = 3600Then --flush-cache when you need the truth immediately.
Mixing sources
Section titled “Mixing sources”A directory can be an inventory, and Ansible merges everything inside it:
inventory/ static.yml ← the database servers, by hand aws_ec2.yml ← the dynamic fleet group_vars/ all.ymlansible-playbook -i inventory/ site.ymlThis is the shape most real estates end up with: machines that were provisioned by something that knows about them come from a plugin, and the handful of long-lived special ones are written down.
Connection variables
Section titled “Connection variables”A small set of variables control how Ansible reaches a host rather than what it should look like. They belong in inventory.
web1.example.com: ansible_host: 10.0.4.12 # actual address; the # inventory name is a label ansible_port: 2222 ansible_user: deploy ansible_become: true ansible_become_method: sudo ansible_python_interpreter: /usr/bin/python3.11 ansible_ssh_common_args: '-o ProxyJump=bastion.example.com'Two of these deserve emphasis.
ansible_host means the inventory name is just a name. You can call a host db-primary and point it at an IP, which makes playbooks readable and survives the address changing.
ansible_ssh_common_args with ProxyJump is how you reach machines through a bastion, and putting it on a group means every host behind that bastion inherits it. This is far cleaner than maintaining a parallel ~/.ssh/config that only works on your laptop.
The localhost you did not define
Section titled “The localhost you did not define”- hosts: localhost connection: local tasks: - ansible.builtin.debug: msg="hello"localhost always exists, even in an empty inventory, and defaults to connection: local — no SSH, no Python payload copy, just running the module in place. This is how you write plays that call APIs, create cloud resources, or do work on the control node. Chapter 11 uses it constantly for delegation.
What to take from this chapter
Section titled “What to take from this chapter”- Inventory is not a list of hosts; it is a set of labels applied to machines, and playbooks are written against the labels.
allandungroupedalways exist. A host sitting inungroupedis usually a typo worth alerting on.- Variables belong in
group_vars/andhost_vars/directories loaded by name — and a filename that matches no group loads for nobody, silently. - Patterns support union, intersection (
:&), exclusion (:!), globs, regex and slices.webservers[0]is your canary. --limitis the most important safety flag in Ansible. It narrows whatever the playbook says.- Run
ansible-inventory --graphbefore trusting a new inventory. - Dynamic inventory turns the cloud into the source of truth. The value is in
keyed_groups— turning tags into the groups your playbooks already use — and you must cache it or you will rate-limit yourself. - A directory can hold static and dynamic sources together, which is what most real estates need.
ansible_hostmakes the inventory name a label, andansible_ssh_common_argswithProxyJumpputs bastion access in the inventory instead of someone’s laptop.localhostalways exists and runs locally without SSH.
Next: the connection and execution model — what “agentless” actually costs, why the default is five hosts at a time, and the two settings that make Ansible several times faster.