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:
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:
# 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:
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:
#!/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:
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
# 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:
#!/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:
- 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
---
- 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
---
- 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:
- •
Tower Deployment:
- →Deployed HA Ansible Tower cluster
- →Configured external PostgreSQL database
- →Set up Redis for caching
- →Implemented backup strategy
- •
Organization Structure:
- →Created organizations per team
- →Implemented RBAC with custom roles
- →Set up credential management
- →Configured audit logging
- •
Automation Standardization:
- →Migrated 200+ playbooks to Tower
- →Created job templates for common tasks
- →Implemented workflow templates
- →Added approval gates
- •
Integration:
- →Integrated with LDAP for authentication
- →Connected to ServiceNow for ticketing
- →Set up Slack notifications
- →Implemented API integrations
- •
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!