Ansible add_host — Create Hosts Dynamically During Play

Introduction

ansible.builtin.add_host adds hosts to the in-memory inventory during playbook execution. This is essential when you provision infrastructure first (cloud instances, containers, VMs) and then need to configure it in the same playbook run. Without add_host, you'd need separate inventory files and multiple playbook runs.

Basic Usage

---
- name: Provision and configure
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Add a host dynamically
      ansible.builtin.add_host:
        name: webserver01
        ansible_host: 192.168.1.100
        ansible_user: ubuntu
        ansible_ssh_private_key_file: ~/.ssh/deploy_key
        groups:
          - webservers
          - production

- name: Configure the new host
  hosts: webservers
  tasks:
    - name: Install Nginx
      ansible.builtin.package:
        name: nginx
        state: present

Cloud Provisioning Pattern

- name: Provision EC2 instances
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Launch instances
      amazon.aws.ec2_instance:
        name: "web-{{ item }}"
        instance_type: t3.micro
        image_id: ami-0abcdef1234567890
        key_name: deploy-key
        state: running
        wait: true
      loop: [1, 2, 3]
      register: ec2_results

    - name: Add instances to inventory
      ansible.builtin.add_host:
        name: "web-{{ item.item }}"
        ansible_host: "{{ item.instances[0].public_ip_address }}"
        ansible_user: ec2-user
        ansible_ssh_private_key_file: ~/.ssh/deploy-key.pem
        groups:
          - webservers
          - "{{ 'primary' if item.item == 1 else 'replicas' }}"
        instance_id: "{{ item.instances[0].instance_id }}"
      loop: "{{ ec2_results.results }}"

    - name: Wait for SSH
      ansible.builtin.wait_for:
        host: "{{ item.instances[0].public_ip_address }}"
        port: 22
        delay: 10
        timeout: 300
      loop: "{{ ec2_results.results }}"

- name: Configure web servers
  hosts: webservers
  become: true
  tasks:
    - name: Install packages
      ansible.builtin.package:
        name:
          - nginx
          - python3
        state: present

- name: Configure primary
  hosts: primary
  tasks:
    - name: Set up as primary
      ansible.builtin.debug:
        msg: "Configuring {{ inventory_hostname }} ({{ instance_id }}) as primary"

Docker Container Pattern

- name: Create and configure containers
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Start containers
      community.docker.docker_container:
        name: "app-{{ item }}"
        image: ubuntu:22.04
        command: sleep infinity
        state: started
      loop: [1, 2, 3]

    - name: Add containers to inventory
      ansible.builtin.add_host:
        name: "app-{{ item }}"
        ansible_connection: community.docker.docker
        groups: containers
      loop: [1, 2, 3]

- name: Configure containers
  hosts: containers
  gather_facts: false
  tasks:
    - name: Install Python
      ansible.builtin.raw: apt-get update && apt-get install -y python3

Setting Host Variables

    - name: Add host with custom variables
      ansible.builtin.add_host:
        name: db-primary
        ansible_host: 10.0.1.50
        ansible_user: postgres
        groups: databases

        # Custom host variables (accessible as hostvars)
        db_role: primary
        db_port: 5432
        db_name: myapp_production
        backup_enabled: true
        max_connections: 200

Multiple Groups

    - name: Add host to multiple groups
      ansible.builtin.add_host:
        name: "{{ server_name }}"
        ansible_host: "{{ server_ip }}"
        groups:
          - all_servers
          - "{{ datacenter }}"          # e.g., us-east-1
          - "{{ server_role }}"         # e.g., webservers
          - "{{ environment }}"         # e.g., production
          - "{{ os_family | lower }}"   # e.g., debian

Conditional Registration

    - name: Discover services on network
      ansible.builtin.command:
        cmd: "nmap -sn 192.168.1.0/24 -oG -"
      register: nmap_result
      changed_when: false
      delegate_to: localhost

    - name: Add discovered hosts
      ansible.builtin.add_host:
        name: "host-{{ item.split()[1] }}"
        ansible_host: "{{ item.split()[1] }}"
        groups: discovered
      loop: "{{ nmap_result.stdout_lines }}"
      when: "'Up' in item"

add_host with run_once

    # add_host always runs on localhost regardless of delegation
    # Use run_once to avoid duplicates when targeting multiple hosts
    - name: Register load balancer
      ansible.builtin.add_host:
        name: lb01
        ansible_host: 10.0.0.1
        groups: loadbalancers
      run_once: true
      changed_when: false  # add_host always reports "changed"

Key Behaviors

BehaviorDetail
Runs onController (always, even with delegate_to)
Changed statusAlways reports "changed" (use changed_when: false)
ScopeCurrent playbook run only (not persisted)
Duplicate namesMerges variables, adds to additional groups
Available whenImmediately — next play can target the host

Troubleshooting

IssueSolution
Host unreachable in next playCheck ansible_host IP and SSH access; add wait_for for port 22
Duplicate "changed" noiseAdd changed_when: false
Host not in expected groupGroup names are case-sensitive; check spelling
Variables not availableAccess via hostvars[hostname].variable_name
SSH key rejectedSet ansible_ssh_private_key_file and ansible_user correctly

Best Practices

  1. Always wait_for SSH — cloud instances need time to boot
  2. Set changed_when: false — add_host always says "changed"
  3. Use run_once — avoid duplicate registrations in multi-host plays
  4. Group strategically — assign roles, environments, and regions as groups
  5. Store instance IDs — as host variables for later operations (terminate, tag)
  6. Pair with group_by — further organize dynamically added hosts

Conclusion

add_host is the bridge between provisioning and configuration in a single playbook run. Provision cloud instances, containers, or VMs in play 1, register them with add_host, then configure them in play 2. It keeps your automation in one file instead of splitting across inventory files and multiple runs.