Managing the list of servers managed by Ansible through static and dynamic inventory, from INI/YAML formats, host grouping, host variables, to host patterns for targeting servers precisely.

After episode 2, where we discussed Ansible's main architecture — from the control node's role as the "brain" running all commands, to managed nodes as the servers being managed, to the execution flow of Python modules over SSH — in this episode we will dissect one of the components we handle most in our day-to-day work: Inventory.
To put it in perspective, inventory is Ansible's address book. Playbooks and ad-hoc commands are just "letters" containing instructions; without a correct address book, those letters will never reach their destination. In the real world, the systems being managed are never just one server. There could be dozens or even thousands of VMs, spread across several environments (staging, production), with different roles (web servers, databases, workers). Inventory is the map that charts all of it, and in this episode you will learn how to build that map correctly.
We will also see why, in the cloud era of relentless auto-scaling, a static inventory file alone is not enough — and how dynamic inventory came about to solve that problem.
Inventory is a file or data source that registers all managed nodes Ansible can manage. Each entry usually contains a host name (or IP) and connection information such as the SSH user, port, and private key.
Important
Inventory is not just a list of server names. It is the source of truth about your infrastructure. The quality of your automation — accuracy, security, and speed — is largely determined by how the inventory is organized.
Ansible supports two formats for writing static inventory: INI (a legacy format from Ansible's early days) and YAML (the modern, recommended format). Both are valid and produce the same behavior. The difference lies in expressiveness and readability as the data structure grows more complex.
[webservers]
web-01.prod.example.com
web-02.prod.example.com
web-01.staging.example.com
[dbservers]
db-01.prod.example.com
db-01.staging.example.com
[prod:children]
webservers
dbservers
[staging:children]
webservers
dbservers
[prod:vars]
ansible_user=deployFrom the example above, you can see both define the same thing. However, there are a few reasons why YAML has become the modern recommendation:
hosts, children, and vars in one neat block.Note
The INI format remains fully supported for backward compatibility, and many older tutorials still use it. It's fine if you find it in documentation, as long as you understand how to map it to YAML structure.
Real systems are almost never "flat". You have web servers, database servers, caches, and workers — each requiring different treatment. Ansible solves this with host groups, labels that hold a set of hosts.
There are two key concepts you must understand:
webservers or dbservers.Notice the hierarchy pattern in the inventory example above: the prod and staging groups are child groups that umbrella webservers and dbservers. With this structure, one ansible prod -m ping command automatically reaches every server in the production environment — without having to name them one by one.
Tip
Organize your inventory along two dimensions at once: role (webserver/database) and environment (prod/staging/dev). This pattern is the best practice used by nearly all infrastructure teams because it enables very flexible pattern matching (we'll cover that shortly).
Every inventory also has two implicit groups that always exist:
all — contains every registered host.ungrouped — hosts that don't belong to any group.A good inventory doesn't stop at host names. Ansible needs to know how to reach those servers. That's the role of host variables — variables whose values apply specifically to one host.
The four connection variables you'll use most often:
| Variable | Function | Example Value |
|---|---|---|
ansible_host | The IP/DNS address SSH uses to connect (the host name in inventory can differ from the actual IP) | 10.0.1.10 |
ansible_user | The SSH user used to log in | deploy |
ansible_port | The SSH port (if not the default 22) | 2222 |
ansible_ssh_private_key_file | Location of the SSH private key file | ~/.ssh/id_ed25519_prod |
all:
hosts:
webserver-prod:
ansible_host: 10.0.1.10
ansible_user: deploy
ansible_port: 22
ansible_ssh_private_key_file: ~/.ssh/id_rsa_prod
webserver-staging:
ansible_host: 10.0.2.10
ansible_user: devops
ansible_ssh_private_key_file: ~/.ssh/id_rsa_staging
children:
dbservers:
hosts:
db-server:
ansible_host: 10.0.1.20
vars:
ansible_user: postgresNotice the example above:
webserver-prod) is just a logical name. The actual IP is defined via ansible_host. This is very useful when a server's IP changes but the logical name stays — your playbooks don't need to change.vars at the group level (ansible_user: postgres for all dbservers) lets one setting apply to many hosts at once. This is a real example of the DRY (Don't Repeat Yourself) principle in inventory.Caution
Credential-related connection variables (e.g., ansible_ssh_pass) should never be hardcoded in an inventory that goes into Git. Use SSH keys or Ansible Vault (we'll cover it in episode 14).
Once the inventory is defined, how do you target a specific subset of hosts? The answer is host patterns — the language for selecting hosts by group and operator.
| Pattern | Meaning | Example Usage |
|---|---|---|
all | Every host in the inventory | ansible all -m ping |
webservers | Every host in the webservers group | ansible webservers -m ping |
web-01.prod.example.com | One specific host by name | ansible web-01.prod.example.com -m ping |
webservers:dbservers | Union — hosts in either group | ansible 'webservers:dbservers' -m ping |
webservers:&staging | Intersection — hosts in both (web servers that are also staging) | ansible 'webservers:&staging' -m ping |
webservers:!production | Exclusion — hosts in webservers but not in production | ansible 'webservers:!production' -m ping |
Tip
For patterns containing :, &, or ! characters, you must wrap them in quotes in the shell. Without quotes, the shell can interpret them as redirects or other operators — one of the most common mistakes in the real world.
A very useful practical combination in daily operations — for example, targeting all web servers in production except one that's having issues:
ansible 'webservers:&production:!web-02.prod.example.com' -m pingansible-inventoryThe bigger the infrastructure, the easier it is to miswrite the inventory. Fortunately, Ansible provides the ansible-inventory utility to validate and introspect inventory structure:
ansible-inventory -i inventory.yml --graphThe --graph form shows the group hierarchy very clearly. If you need per-host variable details, use --list (which can be combined with --yaml for more readable output):
ansible-inventory -i inventory.yml --list --yamlAnother command commonly used for quick validation:
ansible all -i inventory.yml --list-hostsTip
Make it a habit to run ansible-inventory --graph every time you change the inventory structure. Hierarchy errors are much cheaper to find through the graph than through an error while a playbook is running in the middle of peak hours.
All the examples above are static inventory — data written manually in a file. That's fine as long as the number of servers is stable and known in advance. But what about the auto-scaling cloud era?
Imagine an Auto Scaling Group on AWS that can add 5 EC2 instances in the morning when traffic rises and remove them at night. Keeping up with those changes by writing inventory manually is an impossible, slow, and error-prone task. This is why dynamic inventory was born.
Note
Dynamic inventory is inventory whose data is not written manually but generated automatically every time Ansible needs it — usually by querying the cloud provider API directly (AWS, GCP, Azure) or other systems such as a CMDB.
The mechanism is simple: instead of pointing to a static file, dynamic inventory data is fetched at execution time. This keeps the server list always up-to-date with real cloud conditions — exactly like calling a directory service instead of relying on an outdated address card.
aws_ec2 and gcp_computeIn modern Ansible, the dynamic inventory mechanism is implemented through inventory plugins. Plugins are components that know how to talk to a specific provider. The two most popular are amazon.aws.aws_ec2 (AWS EC2) and google.gcp.gcp_compute (Google Cloud Compute Engine).
The plugin configuration is simply defined in a YAML file, and that file is then used as the -i argument:
plugin: amazon.aws.aws_ec2
regions:
- ap-southeast-1
filters:
instance-state-name: running
tag:Environment: production
keyed_groups:
- key: tags.Name
prefix: tag
- key: tags.Role
prefix: role
compose:
ansible_host: public_ip_addressA few important points from the configuration above:
filters — filters which instances enter the inventory, e.g., only running ones in the production environment. This prevents Ansible from trying to reach dead instances.keyed_groups — automatically groups hosts based on cloud metadata (tags/labels). Instances with the Role: web tag automatically enter the role_web group without writing a single line.compose — maps variables from cloud data. For example, ansible_host is taken from the instance's public IP.Important
Plugins like aws_ec2 and gcp_compute are not part of ansible-core, but rather of Collections (amazon.aws and google.gcp). You need to install them first, e.g., via ansible-galaxy collection install amazon.aws. We'll cover Collections in depth in episode 13.
For credential needs, plugins read from standard cloud environment variables (e.g., AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or a service account file. Same as host variables, never store credentials in plugin configuration files that go into Git.
Let's close with a list of mistakes practitioners get stuck on most often:
| Mistake | Symptom | Solution |
|---|---|---|
| Inconsistent YAML indentation | mapping values are not allowed here error during parsing | Watch your indentation; better yet, install the Red Hat YAML extension in your editor |
| Assuming the inventory key is the connectable IP | Failed to connect to the host via ssh error | Define the actual IP via ansible_host |
Pattern containing : without quotes | The shell interprets special characters, wrong target hosts | Always quote patterns: ansible 'webservers:&staging' |
| Referencing a group that was never defined | the host group 'xxx' does not exist error | Run ansible-inventory --graph to verify the structure |
| Hardcoding SSH credentials | Secret leakage in the repository | Use SSH keys + Vault |
In episode 3, we covered inventory comprehensively: INI vs YAML formats, host group hierarchy, host variables for configuring SSH connections, host patterns for targeting servers precisely, verification with ansible-inventory, and an introduction to dynamic inventory for dynamically moving cloud infrastructure.
The essence of this episode is simple yet decisive: the inventory is your infrastructure map, and all subsequent automation skills — ad-hoc commands, playbooks, handlers, and roles — always run on top of the inventory.
In episode 4, we will use this inventory for real practice: Ad-Hoc Commands for daily operational needs, such as checking server status, managing files, installing packages, and restarting services — all from a single command line. Keep your enthusiasm up!