Scaling: dynamic inventory and AWX

Cloud and CMDB inventory plugins, inventory caching, delegation and serial strategies, delegated facts, and a controller with AWX or Automation Platform workflows.

Dynamic inventory

# inventory/aws_ec2.yml: a plugin, not a script
plugin: amazon.aws.aws_ec2
regions: [eu-west-2]
filters:
  tag:Environment: production
  instance-state-name: running
keyed_groups:
  - key: tags.Role
    prefix: role
    separator: _
  - key: placement.availability_zone
    prefix: az
hostnames:
  - tag:Name
  - private-ip-address          # fall back when a name tag is missing
compose:
  ansible_host: private_ip_address
  datacenter: placement.availability_zone
cache: true
cache_plugin: ansible.builtin.jsonfile
cache_timeout: 900
cache_connection: ./.cache/aws_inventory
# a dynamic inventory is reached like any other
ansible-inventory -i inventory/aws_ec2.yml --graph
ansible-inventory -i inventory/aws_ec2.yml --host ec2-host-1
ansible all -i inventory/aws_ec2.yml -m ping --limit role_web

# combine sources: static inventory plus two clouds
# ansible.cfg:
# [defaults]
# inventory = ./inventory/static.ini,./inventory/aws_ec2.yml,./inventory/gcp.yml
  • Enable caching. A plugin that calls the cloud API on every playbook adds seconds to minutes and can hit a rate limit during an incident.
  • compose turns raw API attributes into the variable names your roles already expect, so the playbooks do not need to know the cloud provider.
  • keyed_groups is what makes a dynamic inventory useful for targeting: group by role, environment or zone and then use --limit.
  • A static file for the bastion or the database plus a dynamic plugin for the compute fleet is a normal and healthy combination.

Controlling a large rollout

- name: Rolling restart across the fleet
  hosts: web
  become: true
  serial: "25%"                 # 25 percent of hosts at a time
  max_fail_percentage: 5        # abort the whole rollout if too many fail
  order: sorted                 # deterministic order, easier to reason about
  gather_facts: false
  tasks:
    - name: Drain this host from the load balancer
      ansible.builtin.uri:
        url: "http://lb.internal/api/drain/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      changed_when: false

    - name: Restart the application
      ansible.builtin.systemd:
        name: app
        state: restarted
      notify: Pause for health

    - name: Return this host to rotation
      ansible.builtin.uri:
        url: "http://lb.internal/api/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      changed_when: false

  handlers:
    - name: Pause for health
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:8080/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 20
      delay: 3
      delegate_to: localhost
ToolPurposeNote
serialBatch size for a rolloutA percentage scales better than a number
max_fail_percentageAbort when too many hosts failUse with serial, not instead of it
delegate_toRun a task somewhere elseThe facts stay those of the original host
run_onceRun a task on one hostCombine with delegate_to: localhost
delegate_facts: trueAttach the result to the delegateRarely what you want by default

A controller and workflow

AWX / Automation Platform objects:

  Credential      a machine, source control or cloud credential, stored encrypted
  Inventory       static, sourced from a project file, or a cloud plugin
  Project         a git repository holding the playbooks
  Job Template    project + playbook + inventory + credentials + survey
  Workflow        job templates chained with success, failure and always edges
  Schedule        a cron-like trigger on a job template or workflow

A deployment workflow might be:

  checkout (project sync)
    -> lint and syntax check
    -> deploy to staging
    -> integration test job
    -> approval node (manual, by a named team)
    -> deploy to production  (serial 25%, can fail over to a rollback job)
    -> smoke test
⚠️
A controller multiplatforms the same credential across hundreds of hosts and keeps an audit trail of who ran what — which also means a leaked controller credential is worse than a leaked SSH key. Scope credentials per environment, use the approval node rather than a shared production login, and review who can edit a job template.

FAQ

Dynamic inventory or an inventory built by a job?
A plugin is live and needs no pipeline, but it depends on API access at run time. Generating a static inventory in a scheduled job gives you a reviewable artefact and works when the cloud API is unreachable, at the cost of being stale.
How do I restart a large fleet without downtime?
Take hosts out of rotation in batches with serial, wait for health after each batch, and abort the rollout when a percentage of hosts fail. The load balancer has to be part of the procedure, not an afterthought.

Testing automation: lint, check mode and Molecule Inventories and ad-hoc commands

Last refreshed 2026-09-18.