Ansible: Variables, Loops, and Conditionals - Advanced Automation Techniques
Introduction
While basic Ansible playbooks can automate simple tasks, true infrastructure-as-code mastery requires understanding variables, loops, and conditionals. These three foundational concepts transform rigid automation scripts into flexible, reusable, and intelligent configurations that adapt to different environments, handle complex data structures, and make decisions based on system state.
Variables allow you to parameterize your playbooks, making them work across different environments without code duplication. Loops enable you to perform repetitive tasks efficiently without writing redundant code. Conditionals provide the logic to execute tasks selectively based on facts, variables, or command results. Together, these features enable you to write DRY (Don't Repeat Yourself) playbooks that are maintainable, scalable, and production-ready.
This comprehensive guide explores advanced variable management, sophisticated looping techniques, and intelligent conditional logic in Ansible. You'll learn how to handle complex data structures, implement dynamic configurations, and build playbooks that make intelligent decisions based on your infrastructure's state.
Understanding Ansible Variables
Variable Basics and Naming Conventions
Ansible variables follow specific rules and best practices:
---
# Valid variable names
web_server_port: 80
database_host: db.example.com
app_version: "1.2.3"
enable_ssl: true
max_connections: 100
# Invalid variable names (avoid these)
# web-server-port: 80 # Hyphens not allowed
# 2fa_enabled: yes # Cannot start with number
# user.name: "admin" # Dots not allowed in names
# Best practices for naming
project_name: myapp # Use snake_case
project_environment: production # Be descriptive
project_database_host: db.prod.local # Use prefixes for grouping
Variable Types and Data Structures
Ansible supports various data types:
---
# String variables
server_name: "web-server-01"
description: 'Production web server'
# Numeric variables
port: 8080
max_memory: 2048
percentage: 85.5
# Boolean variables
enable_monitoring: true
debug_mode: false
ssl_enabled: yes # yes/no also work as boolean
# Lists (arrays)
packages:
- nginx
- postgresql
- redis
# Alternative list syntax
web_servers: ["web1.example.com", "web2.example.com", "web3.example.com"]
# Dictionaries (hashes/maps)
database:
host: db.example.com
port: 5432
name: production_db
user: dbadmin
max_connections: 100
# Complex nested structures
application:
name: myapp
version: "2.1.0"
environments:
production:
hosts:
- prod-web-01
- prod-web-02
database:
host: prod-db.example.com
replica: prod-db-replica.example.com
cache:
enabled: true
ttl: 3600
staging:
hosts:
- stage-web-01
database:
host: stage-db.example.com
cache:
enabled: false
Variable Precedence and Scope
Ansible has 22 levels of variable precedence (from lowest to highest):
# Variable precedence (simplified hierarchy):
# 1. Command line values (ansible-playbook -e "var=value")
# 2. Role defaults (roles/myrole/defaults/main.yml)
# 3. Inventory file or script group vars
# 4. Inventory group_vars/all
# 5. Playbook group_vars/all
# 6. Inventory group_vars/*
# 7. Playbook group_vars/*
# 8. Inventory file or script host vars
# 9. Inventory host_vars/*
# 10. Playbook host_vars/*
# 11. Host facts / cached set_facts
# 12. Play vars
# 13. Play vars_prompt
# 14. Play vars_files
# 15. Role vars (roles/myrole/vars/main.yml)
# 16. Block vars (within blocks)
# 17. Task vars (within tasks)
# 18. Include_vars
# 19. Set_facts / registered vars
# 20. Role (and include_role) params
# 21. Include params
# 22. Extra vars (-e "var=value")
Example demonstrating precedence:
---
# group_vars/all.yml
app_port: 8080
# group_vars/webservers.yml
app_port: 8081
# host_vars/web1.example.com.yml
app_port: 8082
# playbook.yml
- name: Variable precedence demo
hosts: web1.example.com
vars:
app_port: 8083 # This takes precedence over group_vars and host_vars
tasks:
- name: Show effective port
debug:
msg: "Application will run on port {{ app_port }}"
# Output: "Application will run on port 8083"
# Command line (highest precedence):
# ansible-playbook playbook.yml -e "app_port=9000"
# Output: "Application will run on port 9000"
Variable Definition and Usage
Defining Variables in Playbooks
---
- name: Variable definition examples
hosts: webservers
# Method 1: vars section
vars:
http_port: 80
https_port: 443
server_name: www.example.com
# Method 2: vars_files
vars_files:
- vars/common.yml
- vars/{{ environment }}.yml
# Method 3: vars_prompt (interactive)
vars_prompt:
- name: deploy_version
prompt: "Which version to deploy?"
private: no
default: "latest"
- name: db_password
prompt: "Database password"
private: yes
encrypt: sha512_crypt
confirm: yes
tasks:
- name: Use variables in tasks
debug:
msg: "Server {{ server_name }} listens on {{ http_port }} and {{ https_port }}"
Accessing Variables with Jinja2
---
- name: Variable access patterns
hosts: all
vars:
simple_var: "hello"
user_info:
name: john
email: [email protected]
roles:
- admin
- developer
servers:
- name: web1
ip: 192.168.1.10
- name: web2
ip: 192.168.1.11
tasks:
# Simple variable access
- name: Simple variable
debug:
msg: "{{ simple_var }}"
# Dictionary access - dot notation
- name: Dictionary with dot notation
debug:
msg: "User: {{ user_info.name }} - Email: {{ user_info.email }}"
# Dictionary access - bracket notation (safer)
- name: Dictionary with brackets
debug:
msg: "User: {{ user_info['name'] }} - Email: {{ user_info['email'] }}"
# List access by index
- name: List element access
debug:
msg: "First role: {{ user_info.roles[0] }}"
# Nested structure access
- name: Nested access
debug:
msg: "First server: {{ servers[0].name }} at {{ servers[0].ip }}"
# Variable in strings (concatenation)
- name: String concatenation
debug:
msg: "The server name is {{ server_name }} and the port is {{ http_port }}"
# Using filters
- name: Variable with filters
debug:
msg: "{{ simple_var | upper }}" # Converts to uppercase
Registering Variables from Task Output
---
- name: Register variables from task results
hosts: localhost
tasks:
- name: Check disk space
command: df -h /
register: disk_space
changed_when: false
- name: Display entire registered variable
debug:
var: disk_space
verbosity: 2
- name: Display specific parts of registered variable
debug:
msg: |
Command: {{ disk_space.cmd }}
Return Code: {{ disk_space.rc }}
Output: {{ disk_space.stdout }}
Error Output: {{ disk_space.stderr }}
- name: Parse command output
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage_percent
changed_when: false
- name: Use registered variable in condition
debug:
msg: "WARNING: Disk usage is {{ disk_usage_percent.stdout }}%"
when: disk_usage_percent.stdout | int > 80
- name: Register complex data
shell: |
cat << EOF
{
"status": "running",
"uptime": "5 days",
"connections": 42
}
EOF
register: service_status
changed_when: false
- name: Parse JSON output
set_fact:
parsed_status: "{{ service_status.stdout | from_json }}"
- name: Use parsed JSON data
debug:
msg: "Service status: {{ parsed_status.status }}, Connections: {{ parsed_status.connections }}"
Using set_fact for Dynamic Variables
---
- name: Dynamic variable creation with set_fact
hosts: all
tasks:
- name: Set simple fact
set_fact:
deployment_time: "{{ ansible_date_time.iso8601 }}"
- name: Set calculated fact
set_fact:
total_memory_gb: "{{ (ansible_memtotal_mb / 1024) | round(2) }}"
- name: Set complex fact
set_fact:
server_info:
hostname: "{{ ansible_hostname }}"
os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
ip: "{{ ansible_default_ipv4.address }}"
memory: "{{ total_memory_gb }} GB"
- name: Use set facts
debug:
msg: "Deployed at {{ deployment_time }} to {{ server_info.hostname }}"
- name: Build fact from multiple sources
set_fact:
deployment_config: |
Hostname: {{ ansible_hostname }}
Environment: {{ environment | default('development') }}
Deployed by: {{ ansible_user_id }}
Timestamp: {{ deployment_time }}
- name: Conditional fact setting
set_fact:
server_role: "{{ 'production' if ansible_hostname.startswith('prod') else 'staging' }}"
- name: Merge dictionaries
set_fact:
combined_config: "{{ default_config | combine(custom_config) }}"
vars:
default_config:
port: 8080
debug: false
max_connections: 100
custom_config:
port: 9000
ssl_enabled: true
Advanced Looping Techniques
Basic Loops with loop
---
- name: Basic loop examples
hosts: all
become: yes
tasks:
# Simple list loop
- name: Install packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis-server
- git
# Loop with variables
- name: Create users
user:
name: "{{ item }}"
state: present
shell: /bin/bash
create_home: yes
loop:
- alice
- bob
- charlie
# Loop with list variable
- name: Create directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop: "{{ directories }}"
vars:
directories:
- /opt/app
- /opt/app/logs
- /opt/app/data
- /opt/app/config
# Loop with range
- name: Create numbered directories
file:
path: "/var/backup/day{{ item }}"
state: directory
loop: "{{ range(1, 8) | list }}" # Creates day1 through day7
Looping Over Dictionaries
---
- name: Dictionary loop examples
hosts: all
become: yes
vars:
users:
alice:
uid: 1001
groups: [sudo, developers]
comment: "Alice Smith"
bob:
uid: 1002
groups: [developers]
comment: "Bob Johnson"
charlie:
uid: 1003
groups: [sudo, operators]
comment: "Charlie Brown"
virtual_hosts:
example.com:
root: /var/www/example.com
port: 80
api.example.com:
root: /var/www/api
port: 8080
tasks:
# Loop over dictionary with dict2items
- name: Create users from dictionary
user:
name: "{{ item.key }}"
uid: "{{ item.value.uid }}"
groups: "{{ item.value.groups | join(',') }}"
comment: "{{ item.value.comment }}"
state: present
loop: "{{ users | dict2items }}"
# Access key and value separately
- name: Configure virtual hosts
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.key }}.conf"
loop: "{{ virtual_hosts | dict2items }}"
notify: reload nginx
# Alternative: use with_dict (older syntax)
- name: Create vhost directories
file:
path: "{{ item.value.root }}"
state: directory
mode: '0755'
with_dict: "{{ virtual_hosts }}"
Complex Nested Loops
---
- name: Nested loop examples
hosts: all
become: yes
vars:
environments:
- name: production
servers:
- web1
- web2
- db1
- name: staging
servers:
- stage-web
- stage-db
firewall_rules:
- chain: INPUT
ports: [80, 443]
protocol: tcp
- chain: INPUT
ports: [22]
protocol: tcp
tasks:
# Nested loop with subelements
- name: Create server-specific files
file:
path: "/etc/config/{{ item.0.name }}/{{ item.1 }}.conf"
state: touch
loop: "{{ environments | subelements('servers') }}"
# Loop with nested structure
- name: Configure firewall rules
ufw:
rule: allow
port: "{{ item.1 }}"
proto: "{{ item.0.protocol }}"
loop: "{{ firewall_rules | subelements('ports') }}"
# Cartesian product (all combinations)
- name: Create all environment-server combinations
debug:
msg: "Creating config for {{ item.0 }} on {{ item.1 }}"
loop: "{{ ['production', 'staging'] | product(['web', 'db', 'cache']) | list }}"
# Flattened nested loop
- name: Install packages for all environments
apt:
name: "{{ item }}"
state: present
loop: "{{ environments | map(attribute='servers') | flatten }}"
Advanced Loop Controls
---
- name: Advanced loop control examples
hosts: all
tasks:
# Loop with index
- name: Show item with index
debug:
msg: "Item {{ ansible_loop.index }}: {{ item }}"
loop:
- first
- second
- third
# Loop with extended attributes
- name: Extended loop info
debug:
msg: |
Current: {{ item }}
Index: {{ ansible_loop.index }}
Index0: {{ ansible_loop.index0 }}
First: {{ ansible_loop.first }}
Last: {{ ansible_loop.last }}
Length: {{ ansible_loop.length }}
Remaining: {{ ansible_loop.revindex }}
loop:
- alpha
- beta
- gamma
# Loop with label (cleaner output)
- name: Loop with custom label
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, uid: 1001, groups: [sudo, dev] }
- { name: bob, uid: 1002, groups: [dev] }
- { name: charlie, uid: 1003, groups: [ops] }
loop_control:
label: "{{ item.name }}" # Only shows name in output
# Pause between loop iterations
- name: Sequential deployment with pause
command: /opt/deploy.sh {{ item }}
loop:
- service1
- service2
- service3
loop_control:
pause: 5 # Wait 5 seconds between iterations
# Loop with custom variable name
- name: Nested loop with custom var names
debug:
msg: "Creating {{ env_name }}-{{ server_name }}"
loop: "{{ environments }}"
loop_control:
loop_var: env_name
vars:
inner_loop:
- web
- db
Looping with until (Retry Logic)
---
- name: Loop with retry logic
hosts: all
tasks:
# Retry until condition is met
- name: Wait for service to be ready
uri:
url: http://localhost:8080/health
status_code: 200
register: result
until: result.status == 200
retries: 10
delay: 5
# Retry with complex condition
- name: Wait for database migration
command: /opt/app/check-migration.sh
register: migration_status
until: >
migration_status.rc == 0 and
'COMPLETED' in migration_status.stdout
retries: 30
delay: 10
changed_when: false
# Retry with exponential backoff simulation
- name: API call with retries
uri:
url: https://api.example.com/deploy
method: POST
body_format: json
body:
version: "{{ app_version }}"
status_code: [200, 201]
register: api_result
until: api_result.status in [200, 201]
retries: 5
delay: "{{ 2 ** (ansible_loop.index0 | default(0)) }}"
# Conditional retry
- name: Check deployment status
shell: kubectl get deployment myapp -o jsonpath='{.status.availableReplicas}'
register: replicas
until: replicas.stdout | int >= 3
retries: 20
delay: 15
changed_when: false
Conditional Execution with when
Basic Conditionals
---
- name: Basic conditional examples
hosts: all
become: yes
vars:
environment: production
enable_monitoring: true
server_role: webserver
tasks:
# Simple equality check
- name: Install production packages
apt:
name:
- monitoring-agent
- log-collector
state: present
when: environment == "production"
# Boolean check
- name: Configure monitoring
template:
src: monitoring.conf.j2
dest: /etc/monitoring/config.conf
when: enable_monitoring
# Multiple conditions (AND)
- name: Configure production web server
template:
src: nginx-prod.conf.j2
dest: /etc/nginx/nginx.conf
when:
- environment == "production"
- server_role == "webserver"
notify: reload nginx
# Multiple conditions (OR)
- name: Install common packages
apt:
name: htop
state: present
when: ansible_distribution == "Ubuntu" or ansible_distribution == "Debian"
# Inequality check
- name: Warn about old OS version
debug:
msg: "WARNING: OS version is outdated"
when: ansible_distribution_version is version('18.04', '<')
# Check if variable is defined
- name: Use optional configuration
template:
src: custom.conf.j2
dest: /etc/app/custom.conf
when: custom_config is defined
# Check if variable is undefined
- name: Set default value
set_fact:
app_port: 8080
when: app_port is not defined
# Check for empty value
- name: Validate required variable
fail:
msg: "database_host is required"
when: database_host is not defined or database_host | length == 0
Conditionals Based on Facts
---
- name: Fact-based conditionals
hosts: all
become: yes
tasks:
# OS family checks
- name: Install packages on Debian systems
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install packages on RedHat systems
yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"
# Distribution-specific tasks
- name: Ubuntu-specific configuration
template:
src: ubuntu-config.j2
dest: /etc/app/config
when: ansible_distribution == "Ubuntu"
# Version-based conditionals
- name: Use systemd on modern systems
systemd:
name: myapp
state: started
enabled: yes
when: ansible_service_mgr == "systemd"
- name: Legacy init.d service
service:
name: myapp
state: started
enabled: yes
when: ansible_service_mgr != "systemd"
# Architecture-based tasks
- name: Install 64-bit specific package
apt:
name: some-package-amd64
state: present
when: ansible_architecture == "x86_64"
# Memory-based decisions
- name: Configure for high-memory server
lineinfile:
path: /etc/app/config
line: "memory_limit=8G"
when: ansible_memtotal_mb > 16000
# Disk space checks
- name: Check available disk space
command: df -h /var | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
changed_when: false
- name: Warn about low disk space
debug:
msg: "WARNING: Disk usage is {{ disk_usage.stdout }}%"
when: disk_usage.stdout | int > 80
# Hostname pattern matching
- name: Configure production servers
template:
src: prod-config.j2
dest: /etc/app/config
when: inventory_hostname is match('prod-.*')
# Multiple fact-based conditions
- name: Optimize for production Ubuntu servers
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
loop:
- { name: 'net.core.somaxconn', value: '4096' }
- { name: 'net.ipv4.tcp_max_syn_backlog', value: '8192' }
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version is version('20.04', '>=')
- ansible_memtotal_mb > 8000
Conditionals with Registered Variables
---
- name: Registered variable conditionals
hosts: all
become: yes
tasks:
# Check command return code
- name: Check if service exists
command: systemctl status myapp
register: service_check
failed_when: false
changed_when: false
- name: Install service if not present
include_tasks: install-service.yml
when: service_check.rc != 0
# Check command output
- name: Get current version
command: /opt/app/bin/version
register: current_version
changed_when: false
- name: Upgrade if version is old
include_tasks: upgrade.yml
when: current_version.stdout is version('2.0.0', '<')
# Check file existence
- name: Check if config exists
stat:
path: /etc/app/config.yml
register: config_file
- name: Create default config
template:
src: default-config.yml.j2
dest: /etc/app/config.yml
when: not config_file.stat.exists
- name: Update config if it exists
lineinfile:
path: /etc/app/config.yml
line: "feature_flag: enabled"
when: config_file.stat.exists
# Check service state
- name: Check if nginx is running
systemd:
name: nginx
register: nginx_status
check_mode: yes
- name: Reload nginx if running
systemd:
name: nginx
state: reloaded
when: nginx_status.status.ActiveState == "active"
# Complex condition with registered variables
- name: Check multiple services
systemd:
name: "{{ item }}"
register: services_status
loop:
- nginx
- postgresql
- redis
check_mode: yes
- name: Report if any service is down
debug:
msg: "Service {{ item.item }} is not running"
loop: "{{ services_status.results }}"
when: item.status.ActiveState != "active"
Advanced Conditional Patterns
---
- name: Advanced conditional patterns
hosts: all
become: yes
vars:
deployment_strategy: rolling
allowed_strategies: [rolling, blue-green, canary]
tasks:
# Check if value in list
- name: Validate deployment strategy
fail:
msg: "Invalid deployment strategy: {{ deployment_strategy }}"
when: deployment_strategy not in allowed_strategies
# String pattern matching
- name: Configure production servers
template:
src: prod.conf.j2
dest: /etc/app/config
when: inventory_hostname is match('prod-web-\d+')
# String contains
- name: Special handling for database servers
include_tasks: db-tasks.yml
when: "'db' in inventory_hostname"
# Type checking
- name: Validate port number
fail:
msg: "Port must be numeric"
when: app_port is not number
# List/dict checks
- name: Process if list has items
include_tasks: process-items.yml
when: item_list is defined and item_list | length > 0
# Conditional with filters
- name: Install on even-numbered servers
debug:
msg: "Installing on {{ inventory_hostname }}"
when: inventory_hostname | regex_search('\d+$') | int is even
# Combined complex conditions
- name: Production deployment gate
assert:
that:
- deployment_approved | default(false)
- backup_completed | default(false)
- rollback_plan_exists | default(false)
fail_msg: "Pre-deployment checks failed"
success_msg: "All deployment gates passed"
when: environment == "production"
# Conditional based on multiple facts
- name: Optimize kernel parameters
sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
loop: "{{ kernel_params | dict2items }}"
when:
- ansible_virtualization_type != "docker"
- ansible_memtotal_mb > 4000
- ansible_processor_vcpus > 2
# Ternary operations
- name: Set environment-specific value
set_fact:
max_connections: "{{ 200 if environment == 'production' else 50 }}"
- name: Configure with ternary
template:
src: app.conf.j2
dest: /etc/app/config
vars:
log_level: "{{ 'info' if environment == 'production' else 'debug' }}"
cache_enabled: "{{ true if ansible_memtotal_mb > 8000 else false }}"
Combining Variables, Loops, and Conditionals
Real-World Example: Multi-Environment Deployment
---
- name: Deploy application across environments
hosts: all
become: yes
vars:
environments:
production:
domain: app.example.com
replicas: 3
resources:
memory: "2Gi"
cpu: "1000m"
features:
caching: true
monitoring: true
debug: false
staging:
domain: staging.app.example.com
replicas: 2
resources:
memory: "1Gi"
cpu: "500m"
features:
caching: true
monitoring: true
debug: true
development:
domain: dev.app.example.com
replicas: 1
resources:
memory: "512Mi"
cpu: "250m"
features:
caching: false
monitoring: false
debug: true
app_version: "{{ deploy_version | default('latest') }}"
tasks:
# Set environment based on inventory group
- name: Determine environment
set_fact:
current_env: "{{ 'production' if 'prod' in group_names else 'staging' if 'stage' in group_names else 'development' }}"
- name: Load environment configuration
set_fact:
env_config: "{{ environments[current_env] }}"
- name: Display deployment info
debug:
msg: |
Deploying to: {{ current_env }}
Domain: {{ env_config.domain }}
Replicas: {{ env_config.replicas }}
Version: {{ app_version }}
# Install environment-specific packages
- name: Install monitoring tools
apt:
name:
- prometheus-node-exporter
- telegraf
state: present
when: env_config.features.monitoring
loop: "{{ env_config.features | dict2items }}"
# Configure based on server role and environment
- name: Configure application
template:
src: app-config.j2
dest: /etc/app/config.yml
vars:
config:
domain: "{{ env_config.domain }}"
debug: "{{ env_config.features.debug }}"
cache:
enabled: "{{ env_config.features.caching }}"
ttl: "{{ 3600 if current_env == 'production' else 60 }}"
database:
pool_size: "{{ 50 if current_env == 'production' else 10 }}"
logging:
level: "{{ 'info' if current_env == 'production' else 'debug' }}"
# Deploy with rolling strategy based on environment
- name: Deploy application
include_tasks: deploy-app.yml
when: inventory_hostname in groups['appservers']
vars:
deployment:
version: "{{ app_version }}"
replicas: "{{ env_config.replicas }}"
strategy: "{{ 'rolling' if current_env == 'production' else 'recreate' }}"
Complex Example: Infrastructure Validation
---
- name: Infrastructure validation and reporting
hosts: all
gather_facts: yes
vars:
requirements:
min_memory_mb: 4096
min_disk_gb: 50
required_ports: [22, 80, 443]
os_family: Debian
min_os_version: "20.04"
compliance_checks:
- name: SSH hardening
command: grep "^PermitRootLogin no" /etc/ssh/sshd_config
- name: Firewall enabled
command: ufw status | grep -q "Status: active"
- name: Automatic updates
command: test -f /etc/apt/apt.conf.d/20auto-upgrades
tasks:
# Gather additional facts
- name: Check disk space
shell: df -BG / | tail -1 | awk '{print $4}' | sed 's/G//'
register: available_disk
changed_when: false
- name: Check open ports
wait_for:
port: "{{ item }}"
timeout: 1
state: started
register: port_checks
failed_when: false
loop: "{{ requirements.required_ports }}"
# Validate requirements with detailed reporting
- name: Validate memory
set_fact:
memory_check:
passed: "{{ ansible_memtotal_mb >= requirements.min_memory_mb }}"
current: "{{ ansible_memtotal_mb }}"
required: "{{ requirements.min_memory_mb }}"
message: "{{ 'OK' if ansible_memtotal_mb >= requirements.min_memory_mb else 'INSUFFICIENT' }}"
- name: Validate disk space
set_fact:
disk_check:
passed: "{{ available_disk.stdout | int >= requirements.min_disk_gb }}"
current: "{{ available_disk.stdout }}"
required: "{{ requirements.min_disk_gb }}"
message: "{{ 'OK' if available_disk.stdout | int >= requirements.min_disk_gb else 'INSUFFICIENT' }}"
- name: Validate OS
set_fact:
os_check:
passed: "{{ ansible_os_family == requirements.os_family and ansible_distribution_version is version(requirements.min_os_version, '>=') }}"
current: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
required: "{{ requirements.os_family }} {{ requirements.min_os_version }}+"
message: "{{ 'OK' if (ansible_os_family == requirements.os_family and ansible_distribution_version is version(requirements.min_os_version, '>=')) else 'INCOMPATIBLE' }}"
# Run compliance checks
- name: Execute compliance checks
shell: "{{ item.command }}"
register: compliance_results
failed_when: false
changed_when: false
loop: "{{ compliance_checks }}"
# Build comprehensive report
- name: Build validation report
set_fact:
validation_report:
hostname: "{{ inventory_hostname }}"
timestamp: "{{ ansible_date_time.iso8601 }}"
requirements:
memory: "{{ memory_check }}"
disk: "{{ disk_check }}"
os: "{{ os_check }}"
compliance:
- name: "{{ item.item.name }}"
status: "{{ 'PASS' if item.rc == 0 else 'FAIL' }}"
- loop: "{{ compliance_results.results }}"
overall_status: "{{ 'COMPLIANT' if (memory_check.passed and disk_check.passed and os_check.passed) else 'NON-COMPLIANT' }}"
- name: Display validation report
debug:
var: validation_report
# Take action based on validation
- name: Fail if non-compliant
fail:
msg: |
Server {{ inventory_hostname }} failed validation:
Memory: {{ memory_check.message }}
Disk: {{ disk_check.message }}
OS: {{ os_check.message }}
when: not (memory_check.passed and disk_check.passed and os_check.passed)
- name: Save report to file
copy:
content: "{{ validation_report | to_nice_json }}"
dest: "/var/log/validation-{{ ansible_date_time.epoch }}.json"
when: validation_report.overall_status == "NON-COMPLIANT"
Best Practices
1. Variable Organization
# Good: Organized variable structure
---
# group_vars/all.yml
company_name: "Example Corp"
base_domain: "example.com"
common_packages:
- curl
- wget
- vim
- git
# group_vars/webservers.yml
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
# host_vars/web1.example.com.yml
server_specific_config:
role: primary
backup_enabled: true
2. Variable Naming Conventions
# Good: Clear, prefixed variable names
mysql_root_password: "secret"
mysql_max_connections: 100
mysql_port: 3306
nginx_worker_processes: 4
nginx_client_max_body_size: "64M"
app_version: "1.2.3"
app_environment: "production"
# Avoid: Generic names that might conflict
password: "secret" # Too generic
port: 3306 # Ambiguous
version: "1.2.3" # Which version?
3. Loop Optimization
---
# Good: Single task with loop
- name: Install multiple packages
apt:
name: "{{ item }}"
state: present
loop: "{{ required_packages }}"
# Avoid: Multiple identical tasks
- name: Install nginx
apt:
name: nginx
state: present
- name: Install postgresql
apt:
name: postgresql
state: present
# ... and so on
4. Conditional Best Practices
---
# Good: Clear, readable conditionals
- name: Install monitoring agent
apt:
name: monitoring-agent
state: present
when:
- environment == "production"
- enable_monitoring | default(true)
- ansible_os_family == "Debian"
# Good: Use meaningful variable names for complex conditions
- name: Determine if deployment should proceed
set_fact:
should_deploy: "{{ (deployment_approved and backup_completed and health_check_passed) }}"
- name: Deploy application
include_tasks: deploy.yml
when: should_deploy
5. Error Handling
---
- name: Safe variable access
debug:
msg: "Database host: {{ database.host | default('localhost') }}"
- name: Validate required variables
assert:
that:
- database_password is defined
- database_password | length > 0
fail_msg: "database_password is required"
- name: Safe dictionary access
set_fact:
db_port: "{{ database.port | default(5432) }}"
when: database is defined
Troubleshooting
Debugging Variables
---
- name: Debug variables
hosts: localhost
tasks:
# Show all variables for host
- name: Display all variables
debug:
var: hostvars[inventory_hostname]
# Show specific variable
- name: Show specific var
debug:
var: ansible_os_family
# Show variable with message
- name: Formatted debug
debug:
msg: "The OS family is {{ ansible_os_family }}"
# Conditional debug with verbosity
- name: Verbose debug
debug:
var: some_complex_variable
verbosity: 2 # Only shows with -vv or higher
Testing Conditionals
# Dry run to test logic
ansible-playbook playbook.yml --check
# Show variable values
ansible-playbook playbook.yml -e "ansible_verbosity=2"
# Test specific tags
ansible-playbook playbook.yml --tags "config" --check --diff
Conclusion
Mastering variables, loops, and conditionals in Ansible is essential for creating flexible, maintainable, and powerful automation. These features transform simple task execution into intelligent infrastructure-as-code that adapts to different environments, handles complex scenarios, and makes data-driven decisions.
Key takeaways:
- Use appropriate variable precedence levels for flexibility
- Organize variables logically in group_vars and host_vars
- Leverage loops to avoid repetitive code
- Apply conditionals to create adaptive playbooks
- Combine these features for sophisticated automation
- Follow naming conventions and best practices
- Test and validate your logic thoroughly
By applying these concepts, you can build robust automation that scales across your entire infrastructure while remaining readable and maintainable.


