Introduction
You know the basics — playbooks, roles, variables, handlers. Now level up with advanced patterns that make your automation more powerful, maintainable, and resilient. These techniques are used in production environments managing thousands of hosts.
Custom Modules
Write Python modules when no existing module fits:
#!/usr/bin/python
# library/app_health.py
from ansible.module_utils.basic import AnsibleModule
import requests
def main():
module = AnsibleModule(
argument_spec=dict(
url=dict(required=True, type='str'),
timeout=dict(default=10, type='int'),
expected_status=dict(default=200, type='int'),
)
)
url = module.params['url']
timeout = module.params['timeout']
expected = module.params['expected_status']
try:
resp = requests.get(url, timeout=timeout)
if resp.status_code == expected:
module.exit_json(
changed=False,
status_code=resp.status_code,
healthy=True,
response_time=resp.elapsed.total_seconds()
)
else:
module.fail_json(
msg=f"Expected {expected}, got {resp.status_code}",
status_code=resp.status_code
)
except requests.exceptions.RequestException as e:
module.fail_json(msg=str(e))
if __name__ == '__main__':
main()
# Use your custom module
- name: Check application health
app_health:
url: "http://localhost:{{ app_port }}/health"
timeout: 5
register: health
- debug:
msg: "Response time: {{ health.response_time }}s"
Custom Filter Plugins
# filter_plugins/custom_filters.py
def to_environment(env_dict):
"""Convert dict to KEY=VALUE environment format."""
return '\n'.join(f'{k}={v}' for k, v in env_dict.items())
def mask_password(text, mask='****'):
"""Mask password in connection strings."""
import re
return re.sub(r'password=[^&\s]+', f'password={mask}', text)
class FilterModule:
def filters(self):
return {
'to_environment': to_environment,
'mask_password': mask_password,
}
- debug:
msg: "{{ db_config | to_environment }}"
# Output: DB_HOST=localhost\nDB_PORT=5432\nDB_NAME=myapp
Complex Data Manipulation
Transform and Merge
# Deep merge configurations
- set_fact:
final_config: "{{ default_config | combine(env_config, recursive=True) | combine(host_config, recursive=True) }}"
# Build dynamic inventory from API
- uri:
url: https://cmdb.example.com/api/servers
return_content: true
register: cmdb
- set_fact:
web_servers: "{{ cmdb.json | selectattr('role', 'eq', 'web') | map(attribute='hostname') | list }}"
db_servers: "{{ cmdb.json | selectattr('role', 'eq', 'db') | map(attribute='hostname') | list }}"
Complex Conditionals
# Multi-condition logic
- name: Deploy only if conditions met
ansible.builtin.include_tasks: deploy.yml
when:
- deploy_enabled | default(true) | bool
- ansible_distribution_version is version('20.04', '>=')
- inventory_hostname not in maintenance_hosts | default([])
- "'canary' not in group_names or canary_approved | default(false)"
JSON Query (JMESPath)
# Complex filtering
- set_fact:
production_apps: "{{ apps | json_query('[?env==`production` && status==`active`].{name: name, port: port}') }}"
high_memory: "{{ instances | json_query('[?memory_gb > `16`]') }}"
grouped: "{{ servers | json_query('[].{region: region, hosts: hosts[?state==`running`].hostname}') }}"
Advanced Delegation
# Run on localhost, loop over remote hosts
- name: Add DNS records for all hosts
community.general.cloudflare_dns:
zone: example.com
record: "{{ item }}"
type: A
value: "{{ hostvars[item]['ansible_host'] }}"
delegate_to: localhost
loop: "{{ groups['webservers'] }}"
run_once: true
# Delegate facts
- name: Get DB version
ansible.builtin.command: psql --version
register: db_version
delegate_to: "{{ groups['dbservers'][0] }}"
delegate_facts: true
run_once: true
Advanced Error Handling
# Retry with exponential backoff
- name: Wait for service with backoff
ansible.builtin.uri:
url: "http://{{ app_host }}:{{ app_port }}/health"
register: result
until: result.status == 200
retries: 10
delay: "{{ item }}"
loop: [1, 2, 4, 8, 16, 30, 30, 30, 30, 30]
# Block with rescue and notification
- block:
- name: Deploy application
include_tasks: deploy.yml
- name: Verify deployment
uri:
url: http://localhost:{{ app_port }}/health
status_code: 200
retries: 10
delay: 5
rescue:
- name: Rollback on failure
include_tasks: rollback.yml
- name: Notify team
uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "⚠️ Deploy FAILED on {{ inventory_hostname }}. Rolled back."
delegate_to: localhost
always:
- name: Record deployment
uri:
url: https://deployments.example.com/api/record
method: POST
body_format: json
body:
host: "{{ inventory_hostname }}"
version: "{{ app_version }}"
status: "{{ 'failed' if ansible_failed_result is defined else 'success' }}"
delegate_to: localhost
ignore_errors: true
Dynamic Role Selection
# Choose role based on variables
- name: Apply web server role
include_role:
name: "{{ web_server_role }}"
vars:
web_server_role: "{{ 'nginx' if use_nginx | default(true) else 'apache' }}"
# Role per OS
- name: Apply OS-specific role
include_role:
name: "{{ ansible_distribution | lower }}_setup"
Custom Facts
# Deploy custom fact script to remote hosts
- name: Deploy custom facts
ansible.builtin.copy:
content: |
#!/bin/bash
echo '{"app_version": "{{ app_version }}", "deploy_date": "{{ ansible_date_time.iso8601 }}", "deployed_by": "ansible"}'
dest: /etc/ansible/facts.d/myapp.fact
mode: "0755"
# Access custom facts
- name: Show app version
debug:
msg: "Version: {{ ansible_local.myapp.app_version }}"
Ansible Vault Advanced
# Encrypt specific variables inline
vars:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
613838...
# Multiple vault IDs
ansible-playbook site.yml --vault-id dev@prompt --vault-id prod@vault-pass-prod
# Vault in lookup
- name: Read encrypted file
debug:
msg: "{{ lookup('file', 'secrets/api_key.vault') }}"
Meta Tasks
# Flush handlers mid-play
- meta: flush_handlers
# End play for current host
- meta: end_host
when: skip_this_host | default(false)
# End entire play
- meta: end_play
when: critical_failure
# Refresh facts
- meta: clear_facts
- setup: # Re-gather
# Clear host errors
- meta: clear_host_errors
Performance Patterns
# Conditional fact gathering (only what you need)
- setup:
gather_subset:
- '!all'
- network
- hardware
# Throttle API calls
- name: Register with API
uri:
url: https://api.example.com/register
method: POST
body_format: json
body: { hostname: "{{ inventory_hostname }}" }
throttle: 5 # Max 5 concurrent
# Pre-cache data on controller
- name: Fetch server list once
uri:
url: https://api.example.com/servers
register: server_list
run_once: true
delegate_to: localhost
Testing Advanced Playbooks
# Assert expected state
- name: Verify deployment
ansible.builtin.assert:
that:
- app_health.status == 200
- "'v{{ app_version }}' in app_health.content"
- db_check.rc == 0
- (disk_check.stdout | int) > 20
fail_msg: "Post-deploy verification failed"
# Molecule with multiple scenarios
molecule test -s docker
molecule test -s vagrant
molecule test -s cloud
Related Articles
- Ansible Best Practices
- Ansible Filter Plugins
- Ansible Error Handling
- Ansible Performance Guide
- Ansible Dynamic Inventory
Conclusion
Advanced Ansible means: custom modules for unique needs, filter plugins for data transformation, JMESPath for complex queries, block/rescue for resilient deployments, dynamic role selection for flexibility, custom facts for host metadata, and meta tasks for flow control. The key shift from beginner to advanced is moving from "tasks that run" to "automation that handles failure gracefully, scales efficiently, and is maintainable by a team." Master these patterns and you can automate anything.