Sign in
← Ansible
16 min · medium · Lesson 10 of 12

Dynamic inventory and collections

Static inventory doesn't scale in the cloud. Inventory plugins (AWS, Yandex Cloud), grouping by tags, and collections from Galaxy.

In the cloud, the list of servers lives its own life: autoscaling adds machines, old ones get removed, IPs change when instances get recreated. The file inventory.ini can't keep up. In this lesson we'll look at how Ansible asks the cloud itself which hosts exist right now, how to turn machine tags into groups, and where the plugins for this come from: collections and ansible-galaxy.

The problem with static inventory in the cloud

Autoscaling brought up three more instances, and inventory.ini doesn't know about them. An old instance got terminated, but its IP is still sitting in the file. Instances get new addresses every time they're recreated.

Keeping inventory.ini up to date by hand is impossible under these conditions: it goes stale faster than you can edit it. You need an inventory that gets generated at playbook run time from a source of truth, that is, from the cloud's API.

Static and dynamic inventory

Static inventory is a file you edit by hand. Ansible just reads the host list from inventory.ini or inventory.yml. This works well when there are few hosts and they don't change.

Dynamic inventory is a host list that Ansible gets from an external system on every run. It asks the cloud which hosts exist right now, and pulls groups, IP addresses and variables from there.

How this works today: inventory plugins

Dynamic inventory used to be done through executable scripts (*.py) that returned JSON with the hosts when run with the --list flag. Ansible called the script and read its output. It worked, but it was awkward: you had to pass the script through -i script.py, maintain its dependencies, and document its parameters.

Today the Ansible docs recommend inventory plugins. A plugin gets a declarative YAML file: where to pull hosts from and how to group them. Ansible talks to the API itself through the plugin. Scripts still work (the built-in script plugin runs them) and show up in older projects, but use plugins for new ones.

The inventory plugin's config

A plugin's config is a plain YAML file. Its first key is plugin: with the plugin's full name, and for cloud plugins the file name has to end with a specific suffix. For EC2 that's aws_ec2.yml or aws_ec2.yaml, for example inventory.aws_ec2.yml. The plugin won't accept a file with a different name.

You call Ansible through the -i flag, same as with static inventory, just pointing at the plugin's YAML file or a directory with several files:

ansible-playbook -i inventory.aws_ec2.yml site.yml

Example: AWS EC2

The amazon.aws.aws_ec2 plugin ships in the amazon.aws collection and isn't part of ansible-core. The machine that runs Ansible also needs the Python libraries boto3 and botocore.

# inventory.aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - eu-west-1
  - us-east-1

filters:
  instance-state-name: running
  "tag:Environment": production

keyed_groups:
  - key: tags.Role
    prefix: tag_role
  - key: tags.Environment
    prefix: tag_env

compose:
  ansible_host: public_ip_address

What's happening here:

If an instance has the tag Role=web, Ansible creates the group tag_role_web: it appends the separator _ and the tag's value to the prefix tag_role. In a playbook you can write hosts: tag_role_web and land on every web server, however many there are.

For AWS authentication, the plugin reads standard AWS credentials: from the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, from ~/.aws/credentials, or from an IAM Instance Profile.

Grouping by tags: the main payoff

keyed_groups is usually the whole reason people set up dynamic inventory in the cloud.

You tag instances when creating them (Terraform, CloudFormation, or by hand), say Role=web, Role=db, Environment=staging. Ansible turns the tags into groups: tag_role_web, tag_role_db, tag_env_staging.

In the playbook you write:

- name: Deploy app
  hosts: tag_role_web
  roles:
    - nginx
    - app

Ten new web servers come up, and the playbook sees them on the next run. No need to touch the inventory.

compose computes a variable's value from the instance's attributes. For example:

compose:
  ansible_host: public_ip_address
  # or for a private network:
  # ansible_host: private_ip_address

This is needed because the instance's inventory name (usually an instance ID like i-0a1b2c3d) isn't an IP address. Without compose, Ansible wouldn't know where to connect over SSH.

The key values in keyed_groups and the right-hand sides of compose are Jinja2 expressions over host variables, that is, over whatever the plugin got from the API.

The constructed plugin: groups from variables

keyed_groups, compose and groups aren't only for cloud plugins. In the built-in ansible.builtin.constructed plugin they work on top of an inventory that's already loaded: it takes hosts and variables from other sources and builds groups from them.

That lets you try grouping without a cloud. You put a file with hosts into the inventory/ directory, and a plugin file next to it:

# inventory/02-groups.yml
plugin: ansible.builtin.constructed
strict: true
keyed_groups:
  - key: os
    prefix: os
groups:
  big: memory_mb > 4096

Ansible reads the directory's files alphabetically, so the file with hosts is named to come first. A host with the variable os: debian lands in the group os_debian, a host with memory_mb: 8192 lands in big.

strict defaults to false. In this mode, an error in an expression (say, referencing a key that doesn't exist) is dropped silently by the plugin, and the group simply doesn't appear. With strict: true Ansible immediately prints which expression failed to evaluate and for which host.

Yandex Cloud and other providers

Most cloud providers have their own inventory plugins in collections: GCP (google.cloud.gcp_compute), Azure (azure.azcollection.azure_rm), DigitalOcean, Hetzner, and others. Look up the provider's collection on galaxy.ansible.com and read its docs: the principle is the same, a YAML config with filters and grouping.

Yandex Cloud has no official collection with an inventory plugin on Galaxy (checked in October 2026). The usual workaround: grab a third-party plugin from GitHub, or write a script on top of yc compute instance list --format json that outputs hosts in the inventory-script format.

Debugging: what the plugin actually resolved

When working with dynamic inventory, the first thing to do is check what came out:

# The whole inventory as JSON: hosts, groups, variables
ansible-inventory -i inventory.aws_ec2.yml --list

# Group and host tree, handy for a quick check
ansible-inventory -i inventory.aws_ec2.yml --graph

# One host's variables
ansible-inventory -i inventory.aws_ec2.yml --host i-0a1b2c3d

--graph gives you roughly this:

@all:
  |--@ungrouped:
  |--@tag_role_web:
  |  |--i-0a1b2c3d
  |  |--i-0e4f5a6b
  |--@tag_role_db:
  |  |--i-0c7d8e9f

If you see something other than what you expected, check --host or --list: they show all the host's variables, including ansible_host and the tags the groups are built from.

Collections: where plugins and modules come from

Starting with Ansible 2.10, most modules and plugins live in collections, not in Ansible's core. A collection is a separate package with modules, plugins (including inventory plugins), roles and filters under one namespace.

Name format: namespace.collection. Examples:

CollectionWhat's inside
amazon.awsEC2, S3, RDS, IAM, and the aws_ec2 inventory plugin
community.generalhundreds of modules for various systems and services
ansible.posixmodules for POSIX systems: authorized_key, mount, sysctl
kubernetes.coremodules for Kubernetes
community.dockerdocker_container, docker_image, and others

Modules are called by their full name, FQCN (Fully-Qualified Collection Name): amazon.aws.ec2_instance, community.general.ufw, ansible.posix.authorized_key. That way modules with the same short name from different collections don't collide.

In playbooks you can write short names (ec2_instance) if the collection is listed in the play's collections: block, but FQCN is more explicit and more reliable.

Installing collections with ansible-galaxy

# Install one collection
ansible-galaxy collection install amazon.aws

# Install a specific version
ansible-galaxy collection install amazon.aws:==8.0.0

By default collections are installed into ~/.ansible/collections/ansible_collections/.

For reproducible environments, pin dependencies in requirements.yml. This file has two separate blocks, one for collections and one for roles:

# requirements.yml
collections:
  - name: amazon.aws
    version: ">=8.0.0"
  - name: community.general
    version: ">=9.0.0"
  - name: ansible.posix

roles:
  - name: geerlingguy.nginx
    version: "3.2.0"
  - name: geerlingguy.postgresql
    version: "3.4.3"

Roles and collections are installed by different subcommands:

ansible-galaxy role install -r requirements.yml        # only the roles: block
ansible-galaxy collection install -r requirements.yml  # only the collections: block

The command ansible-galaxy install -r requirements.yml installs both blocks at once. An exception from the docs: if collections have their own install path set, one command won't do it, so run both.

Roles and collections: the difference

A role is one reusable unit: tasks, handlers, templates and variables for a single job (say, "configure nginx"). A role doesn't contain its own modules or plugins.

A collection is a package that can hold everything at once: modules, plugins (including inventory plugins), filters, roles and their docs, under one namespace. amazon.aws is a collection that has both modules (ec2_instance, s3_object) and an inventory plugin (aws_ec2).

Writing reusable automation means writing a role. Developing new modules, or wanting to bundle roles and modules under one name, means writing a collection.

Diagram: how dynamic inventory works

CLOUD API AWS / YC / GCP instances + tags INVENTORY PLUGIN amazon.aws.aws_ec2 keyed_groups + compose inventory.aws_ec2.yml GROUP: tag_role_web web-01 web-02 web-03 hosts: tag_role_web GROUP: tag_role_db db-01 db-02 hosts: tag_role_db ansible-inventory --graph shows the grouping result

Common mistakes

What you'll need for the tasks

There's no cloud in the labs: hosts and their tags live in a YAML file that plays the role of the cloud API's answer. Grouping, compose and debugging work the same way as with aws_ec2.

Next up: secrets, Ansible Vault.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson ansible-basics-10-dynamic-inventory