# How to use Ansible dynamic inventory on Quake AI

Source: https://docs.quake.ai/docs/automation/how-to/ansible-dynamic-inventory
Markdown: https://docs.quake.ai/docs/automation/how-to/ansible-dynamic-inventory.md

---

# How to use Ansible dynamic inventory on Quake AI

This guide replaces hand-maintained inventory files with the `openstack.cloud.openstack` plugin, which queries Quake AI directly and groups hosts by metadata. By the end you will have an inventory that reflects the live state of your project: new instances appear automatically, terminated instances drop out, and metadata tags become Ansible groups you can target with `--limit`.

For the wider context (when to provision with OpenTofu and configure with Ansible), read [Ansible on Quake AI](/docs/automation/concepts/ansible).

## Prerequisites

- [Ansible setup](/docs/automation/how-to/getting-started-ansible) complete: `ansible`, `openstacksdk >= 1.0.0`, the `openstack.cloud` collection installed
- A named cloud (`rumble`) configured in `~/.config/openstack/clouds.yaml`
- One or more Quake AI instances tagged with metadata (`environment`, `role`, or any keys you want to group on). If you have nothing running yet, the [getting started IaC guide](/docs/automation/how-to/getting-started-iac) and the [OpenTofu plus Ansible workflow](/docs/automation/how-to/ansible-opentofu-workflow) both create instances with metadata.

## Why dynamic inventory

Static inventory files become a maintenance burden the moment a fleet grows past a handful of hosts. Every new instance needs a line; every retired instance leaves a stale entry; metadata changes mean editing the file in two places.

The dynamic inventory plugin sidesteps that by making Quake AI the source of truth. Ansible queries the Identity and Compute APIs at runtime, builds the inventory from running instances, and groups them by metadata you set on the instances themselves. The same playbooks then target those groups without anyone editing an inventory file.

## Install the plugin requirements

The plugin ships with the `openstack.cloud` collection installed in the [getting-started guide](/docs/automation/how-to/getting-started-ansible). Verify both pieces are present:

```bash
ansible-galaxy collection list openstack.cloud
python3 -c "import openstack; print(openstack.__version__)"
```

You should see `openstack.cloud` 2.2.0 or newer and `openstacksdk` 1.0.0 or newer. If `openstacksdk` is missing or older than 1.0.0, upgrade it before continuing:

```bash
pip3 install --user --upgrade "openstacksdk>=1.0.0"
```

## Author the inventory configuration

Create a project directory and an inventory file:

```bash
mkdir ansible-inventory-demo && cd ansible-inventory-demo
mkdir inventory
```

Create `inventory/openstack.yml`:

```yaml
plugin: openstack.cloud.openstack
clouds:
  - rumble
expand_hostvars: false
inventory_hostname: name
compose:
  ansible_user: "'ubuntu'"
keyed_groups:
  - key: openstack.metadata.environment
    prefix: env
  - key: openstack.metadata.role
    prefix: role
  - key: openstack.flavor.name
    prefix: flavor
  - key: openstack.image.name
    prefix: image
```

The file selects the named cloud (`rumble`) from your `clouds.yaml`, sets the Ansible hostname to the instance name, sets `ansible_user` to `ubuntu` (quoted as a Jinja string literal) to match the Quake AI Ubuntu cloud images, and produces four groups per instance: `env_<environment>`, `role_<role>`, `flavor_<flavor>`, and `image_<image>`. The plugin sets `ansible_host` to the floating IP when one is attached.

The plugin also emits its own auto-generated `meta-<key>_<value>` groups for every instance metadata pair (e.g. `meta-environment_production`, `meta-role_web`). It also builds built-in groups such as `flavor-m2a.large` (hyphen prefix, dots preserved). Either the explicit `env_*`/`role_*` groups or the `meta-*` siblings work as playbook targets; pick whichever reads more clearly in your `hosts:` line.

Instances booted from a Cinder volume have an empty `openstack.image` hostvar, so the `image_*` keyed group does not form for them even when the instance runs normally.

The credential flow is the same one you already configured for the modules. The plugin reads `clouds.yaml` and the `OS_*` environment variable chain. Re-document of that flow lives in the [getting-started guide](/docs/automation/how-to/getting-started-ansible#configure-authentication).

## Verify the inventory

List every host the plugin can see:

```bash
ansible-inventory -i inventory/openstack.yml --list
```

The output is a JSON document. Each instance appears under the groups derived from its metadata, flavor, and image. Use `--graph` for a more readable view:

```bash
ansible-inventory -i inventory/openstack.yml --graph
```

You should see explicit groups like `env_production` and `role_web` from `keyed_groups`, alongside the plugin's auto-generated `meta-environment_production`, `meta-role_web`, and built-in groups such as `flavor-m2a.large`. The `image_*` keyed group appears only when `openstack.image.name` is populated (volume-booted instances omit it).

If the inventory comes back empty, common causes are:

- The named cloud in `inventory/openstack.yml` does not match an entry in `clouds.yaml`
- The application credential lacks read access to the project's compute resources
- All instances are in a non-`ACTIVE` state (the plugin filters on running instances by default)

## Filter the inventory to the current project

The `openstack.cloud.openstack` plugin enumerates every server visible to the application credential. If the credential carries access to a shared project, the inventory enumerates unrelated VMs and `ansible-playbook` against an unfiltered group can target hosts you do not own.

The safest pattern is the application-credential scoping that the [Generate app credentials](/docs/tools/generate-app-credentials) flow already produces: an app credential created inside a single project lists only that project's servers. Confirm the cloud's `auth.application_credential_id` in `clouds.yaml` matches the credential you generated for the project you intend to target.

For a second layer of safety, restrict the inventory to instances tagged with a project-specific metadata key. Add a `filters:` block to `inventory/openstack.yml`:

```yaml
plugin: openstack.cloud.openstack
clouds:
  - rumble
expand_hostvars: false
inventory_hostname: name
compose:
  ansible_user: "'ubuntu'"
filters:
  metadata:
    project: ansible-demo
keyed_groups:
  - key: openstack.metadata.environment
    prefix: env
  - key: openstack.metadata.role
    prefix: role
```

Set the `project` metadata key on every instance you provision (`metadata = { project = "ansible-demo" }` in OpenTofu, or `meta: { project: ansible-demo }` on the `openstack.cloud.server` task). The filter drops any instance that does not match, so an inventory pulled from a shared project surfaces only the matching VMs.

Pair the filter with `--limit env_production` or `--limit role_web` on the `ansible-playbook` invocation so a typo in the inventory cannot fan out a play across the wrong group.

## Group instances with `keyed_groups`

The `keyed_groups` block translates instance attributes into Ansible groups. Anything visible to the OpenStack API can become a group key. The most useful ones for Quake AI:

| Key | Group prefix example | When to use |
|---|---|---|
| `openstack.metadata.environment` | `env_production` | Separate dev, staging, and production fleets |
| `openstack.metadata.role` | `role_web` | Target web hosts vs database hosts |
| `openstack.flavor.name` | `flavor_m2a_large` | Apply config that depends on instance size |
| `openstack.image.name` | `image_ubuntu_24_04` | Branch tasks by base image (empty for volume-booted instances) |
| `openstack.metadata.team` | `team_platform` | Multi-team projects sharing one Quake AI project |

Set instance metadata at provisioning time. With OpenTofu:

```hcl
resource "openstack_compute_instance_v2" "web" {
  name = "web-01"

  metadata = {
    environment = "production"
    role        = "web"
    team        = "platform"
  }
}
```

Or with the `openstack.cloud.server` module (see the [provisioning guide](/docs/automation/how-to/ansible-provision-instance)).

For finer control, combine `keyed_groups` with `groups` (boolean expressions) and `compose` (computed host vars). The full schema lives in the [`openstack.cloud.openstack` plugin documentation](https://docs.ansible.com/ansible/latest/collections/openstack/cloud/openstack_inventory.html).

## Run a playbook against the dynamic inventory

Create `site.yml`:

```yaml
---
- name: Configure production web hosts
  hosts: env_production:&role_web
  become: true

  tasks:
    - name: Wait for SSH
      ansible.builtin.wait_for_connection:
        timeout: 300

    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
```

The `hosts: env_production:&role_web` selector targets instances that are in both groups (production environment and web role). Run it:

```bash
ansible-playbook -i inventory/openstack.yml site.yml
```

To narrow the run further on the command line, use `--limit`:

```bash
ansible-playbook -i inventory/openstack.yml site.yml --limit env_production
ansible-playbook -i inventory/openstack.yml site.yml --limit web-01
```

The same inventory file works for ad-hoc commands:

```bash
ansible -i inventory/openstack.yml env_staging -m ansible.builtin.ping
```

## Tune performance with `expand_hostvars`

By default, the plugin returns the basic instance attributes that Ansible needs to connect: name, address, image, flavor, metadata. With `expand_hostvars: true`, the plugin makes additional API calls per host to fetch volume and port details. That extra information is occasionally useful (for example, when a playbook needs to read the attached volume names), but the per-host API calls become a bottleneck on large fleets.

Recommendation: leave `expand_hostvars: false` for routine operations. Set it to `true` only when a playbook explicitly needs Cinder volume or Neutron port details that are not already in metadata.

For multi-step pipelines that re-run the same inventory query many times, enable caching:

```yaml
plugin: openstack.cloud.openstack
clouds:
  - rumble
cache: true
cache_plugin: jsonfile
cache_connection: ~/.cache/ansible/inventory
cache_timeout: 300
```

A 300-second cache eliminates redundant API calls during a single CI pipeline run while still picking up fleet changes between runs.

## Tear down

The dynamic inventory plugin does not create any state. To stop using it, point your `ansible-playbook` invocations back at a static inventory file or remove the `inventory/openstack.yml` file from your project. The Quake AI instances are unaffected.

## See also

- [Ansible on Quake AI](/docs/automation/concepts/ansible)
- [How to get started with Ansible on Quake AI](/docs/automation/how-to/getting-started-ansible)
- [How to use Ansible with OpenTofu on Quake AI](/docs/automation/how-to/ansible-opentofu-workflow)
- [How to provision a Quake AI instance with Ansible](/docs/automation/how-to/ansible-provision-instance)
- [Generate app credentials](/docs/tools/generate-app-credentials)
- [`openstack.cloud.openstack` inventory plugin](https://docs.ansible.com/ansible/latest/collections/openstack/cloud/openstack_inventory.html)
