All posts

Advanced Ansible: Dynamic Inventory and Tower

· Pavan Y N · ansible, advanced, tower, dynamic-inventory

Published: October 30, 2025
Reading Time: 20 minutes
Tags: Ansible, Advanced, Tower, AWX, Dynamic Inventory

# Introduction

Advanced Ansible techniques enable enterprise-scale automation with dynamic inventory, Ansible Tower/AWX, custom modules, and sophisticated orchestration patterns. This guide covers advanced Ansible features for managing large-scale infrastructure.

# Dynamic Inventory

## AWS EC2 Dynamic Inventory

aws_ec2.yml:

code
plugin: aws_ec2
regions:
  - us-east-1
  - us-west-2
  - eu-west-1

filters:
  instance-state-name: running
  tag:Managed: ansible

keyed_groups:
  # Group by instance type
  - key: instance_type
    prefix: type
  
  # Group by environment tag
  - key: tags.Environment
    prefix: env
  
  # Group by application tag
  - key: tags.Application
    prefix: app
  
  # Group by availability zone
  - key: placement.availability_zone
    prefix: az

hostnames:
  - tag:Name
  - private-ip-address

compose:
  ansible_host: public_ip_address
  ansible_user: "'ec2-user' if 'amazon' in image_id else 'ubuntu'"
  
groups:
  production: "'production' in tags.Environment"
  staging: "'staging' in tags.Environment"
  webservers: "'web' in tags.Role"
  databases: "'db' in tags.Role"

Usage:

code
# List inventory
ansible-inventory -i aws_ec2.yml --list

# Graph inventory
ansible-inventory -i aws_ec2.yml --graph

# Run playbook with dynamic inventory
ansible-playbook -i aws_ec2.yml deploy.yml

## Azure Dynamic Inventory

azure_rm.yml:

code
plugin: azure_rm

auth_source: auto

include_vm_resource_groups:
  - production-rg
  - staging-rg

keyed_groups:
  - prefix: tag
    key: tags
  - prefix: azure_loc
    key: location
  - prefix: azure_os
    key: os_profile.system

conditional_groups:
  linux: "'Linux' in os_profile.system"
  windows: "'Windows' in os_profile.system"
  production: "'production' in tags.environment"

## Custom Dynamic Inventory Script

inventory.py:

code
#!/usr/bin/env python3

import json
import sys
import requests

def get_inventory():
    # Fetch inventory from API
    response = requests.get('https://api.example.com/inventory')
    data = response.json()
    
    inventory = {
        '_meta': {
            'hostvars': {}
        }
    }
    
    for server in data['servers']:
        # Add to groups
        for group in server['groups']:
            if group not in inventory:
                inventory[group] = {
                    'hosts': [],
                    'vars': {}
                }
            inventory[group]['hosts'].append(server['hostname'])
        
        # Add host variables
        inventory['_meta']['hostvars'][server['hostname']] = {
            'ansible_host': server['ip_address'],
            'ansible_user': server['ssh_user'],
            'environment': server['environment'],
            'application': server['application']
        }
    
    return inventory

def get_host(hostname):
    # Fetch specific host variables
    response = requests.get(f'https://api.example.com/hosts/{hostname}')
    return response.json()

if __name__ == '__main__':
    if len(sys.argv) == 2 and sys.argv[1] == '--list':
        print(json.dumps(get_inventory(), indent=2))
    elif len(sys.argv) == 3 and sys.argv[1] == '--host':
        print(json.dumps(get_host(sys.argv[2]), indent=2))
    else:
        print("Usage: inventory.py --list or inventory.py --host <hostname>")
        sys.exit(1)

# Ansible Tower / AWX

## Installation

Docker Compose:

code
version: '3.8'

services:
  awx_web:
    image: ansible/awx:latest
    container_name: awx_web
    depends_on:
      - awx_postgres
      - awx_redis
    ports:
      - "80:8052"
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_USER=awx
      - DATABASE_PASSWORD=${DB_PASSWORD}
      - DATABASE_NAME=awx
      - DATABASE_PORT=5432
      - DATABASE_HOST=awx_postgres
      - REDIS_HOST=awx_redis
      - REDIS_PORT=6379
    volumes:
      - awx_projects:/var/lib/awx/projects
      - awx_rsyslog:/var/log/tower

  awx_task:
    image: ansible/awx:latest
    container_name: awx_task
    depends_on:
      - awx_postgres
      - awx_redis
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_USER=awx
      - DATABASE_PASSWORD=${DB_PASSWORD}
      - DATABASE_NAME=awx
      - DATABASE_PORT=5432
      - DATABASE_HOST=awx_postgres
      - REDIS_HOST=awx_redis
      - REDIS_PORT=6379
    volumes:
      - awx_projects:/var/lib/awx/projects

  awx_postgres:
    image: postgres:13
    container_name: awx_postgres
    environment:
      - POSTGRES_USER=awx
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=awx
    volumes:
      - awx_postgres:/var/lib/postgresql/data

  awx_redis:
    image: redis:7-alpine
    container_name: awx_redis

volumes:
  awx_postgres:
  awx_projects:
  awx_rsyslog:

## AWX CLI Configuration

code
# Install AWX CLI
pip install awxkit

# Configure AWX CLI
awx login -f human \
  --conf.host https://awx.example.com \
  --conf.username admin \
  --conf.password password

# Create organization
awx organizations create \
  --name "MyOrg" \
  --description "My Organization"

# Create inventory
awx inventory create \
  --name "Production" \
  --organization "MyOrg"

# Create project
awx projects create \
  --name "MyProject" \
  --organization "MyOrg" \
  --scm_type git \
  --scm_url "https://github.com/myorg/ansible-playbooks.git" \
  --scm_branch main

# Create job template
awx job_templates create \
  --name "Deploy Application" \
  --job_type run \
  --inventory "Production" \
  --project "MyProject" \
  --playbook "deploy.yml" \
  --credentials "SSH Key"

# Launch job
awx job_templates launch "Deploy Application"

# Custom Modules

## Simple Custom Module

library/check_port.py:

code
#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
import socket

def check_port(host, port, timeout=5):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except Exception as e:
        return False

def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type='str', required=True),
            port=dict(type='int', required=True),
            timeout=dict(type='int', default=5)
        ),
        supports_check_mode=True
    )

    host = module.params['host']
    port = module.params['port']
    timeout = module.params['timeout']

    if module.check_mode:
        module.exit_json(changed=False)

    is_open = check_port(host, port, timeout)

    if is_open:
        module.exit_json(
            changed=False,
            msg=f"Port {port} is open on {host}",
            port_open=True
        )
    else:
        module.fail_json(
            msg=f"Port {port} is closed on {host}",
            port_open=False
        )

if __name__ == '__main__':
    main()

Usage:

code
- name: Check if port is open
  check_port:
    host: example.com
    port: 443
    timeout: 10
  register: port_check

- name: Display result
  debug:
    msg: "Port is {{ 'open' if port_check.port_open else 'closed' }}"

# Advanced Playbook Patterns

## Rolling Updates with Serial

code
---
- name: Rolling update with health checks
  hosts: webservers
  serial: "25%"
  max_fail_percentage: 25
  
  pre_tasks:
    - name: Remove from load balancer
      delegate_to: localhost
      uri:
        url: "http://lb.example.com/api/remove/{{ inventory_hostname }}"
        method: POST
      register: lb_remove
      until: lb_remove.status == 200
      retries: 3
      delay: 5
  
  tasks:
    - name: Stop application
      service:
        name: myapp
        state: stopped
    
    - name: Deploy new version
      copy:
        src: /tmp/myapp-v2.0.tar.gz
        dest: /opt/myapp/
    
    - name: Start application
      service:
        name: myapp
        state: started
    
    - name: Wait for application to be ready
      wait_for:
        port: 8080
        delay: 10
        timeout: 60
    
    - name: Run health check
      uri:
        url: "http://{{ inventory_hostname }}:8080/health"
        status_code: 200
      register: health_check
      until: health_check.status == 200
      retries: 5
      delay: 10
  
  post_tasks:
    - name: Add back to load balancer
      delegate_to: localhost
      uri:
        url: "http://lb.example.com/api/add/{{ inventory_hostname }}"
        method: POST
      register: lb_add
      until: lb_add.status == 200
      retries: 3
      delay: 5

## Delegation and Run Once

code
---
- name: Database migration
  hosts: webservers
  
  tasks:
    - name: Run database migration (once)
      command: /opt/myapp/migrate.sh
      run_once: true
      delegate_to: "{{ groups['databases'][0] }}"
    
    - name: Clear cache on all nodes
      command: /opt/myapp/clear-cache.sh
    
    - name: Notify monitoring system
      uri:
        url: "https://monitoring.example.com/api/deployment"
        method: POST
        body_format: json
        body:
          application: myapp
          version: "{{ app_version }}"
          hosts: "{{ groups['webservers'] }}"
      delegate_to: localhost
      run_once: true

# STAR Approach: Ansible Tower Implementation

Situation: Company with 50+ teams managing 5000+ servers, inconsistent automation, and no centralized control.

Task: Implement Ansible Tower for centralized automation, RBAC, and self-service provisioning.

Action:

  1. •

    Tower Deployment:

    • →Deployed HA Ansible Tower cluster
    • →Configured external PostgreSQL database
    • →Set up Redis for caching
    • →Implemented backup strategy
  2. •

    Organization Structure:

    • →Created organizations per team
    • →Implemented RBAC with custom roles
    • →Set up credential management
    • →Configured audit logging
  3. •

    Automation Standardization:

    • →Migrated 200+ playbooks to Tower
    • →Created job templates for common tasks
    • →Implemented workflow templates
    • →Added approval gates
  4. •

    Integration:

    • →Integrated with LDAP for authentication
    • →Connected to ServiceNow for ticketing
    • →Set up Slack notifications
    • →Implemented API integrations
  5. •

    Self-Service Portal:

    • →Built custom web interface
    • →Enabled self-service provisioning
    • →Implemented request approval workflow
    • →Created comprehensive documentation

Result:

  • →Centralized control of all automation
  • →Reduced unauthorized changes by 100%
  • →Self-service enabled for 500+ users
  • →Automation execution time reduced by 40%
  • →Audit compliance improved to 100%
  • →Mean time to provision reduced from days to minutes

# Best Practices

## 1. Inventory Management

  • →Use dynamic inventory for cloud
  • →Implement proper grouping
  • →Use host and group variables effectively
  • →Regular inventory audits

## 2. Playbook Design

  • →Keep playbooks idempotent
  • →Use roles for reusability
  • →Implement proper error handling
  • →Add comprehensive logging

## 3. Security

  • →Use Ansible Vault for secrets
  • →Implement RBAC in Tower
  • →Regular credential rotation
  • →Audit all changes

## 4. Performance

  • →Use fact caching
  • →Implement parallel execution
  • →Optimize task execution
  • →Monitor resource usage

## 5. Maintenance

  • →Regular Tower upgrades
  • →Backup configurations
  • →Monitor job execution
  • →Clean up old data

# Conclusion

Advanced Ansible patterns with Tower/AWX enable enterprise-scale automation with centralized control, security, and efficiency. By leveraging dynamic inventory, custom modules, and sophisticated orchestration, teams can build robust automation that scales with organizational needs.

# Additional Resources


Ready to scale Ansible? Start with dynamic inventory and gradually implement Tower for centralized management!