Conditionals and Loops
Introduction
Conditionals and loops are fundamental control structures in Ansible that allow you to execute tasks selectively and repeat operations efficiently. Mastering these concepts enables you to write flexible, dynamic playbooks that adapt to different environments and handle complex automation scenarios.
- Conditionals: Execute tasks based on specific criteria using
whenstatements - Loops: Repeat tasks across multiple items with
loop,with_*, oruntil - Filters: Transform data with Jinja2 filters for advanced conditions
- Loop Controls: Fine-tune loop execution with labels, pauses, and breaks
Conditionals with When
Basic When Statements
The when clause allows tasks to run only when specific conditions are met:
---
- name: Conditional execution examples
hosts: all
gather_facts: yes
tasks:
- name: Install Apache on Red Hat family
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Install Apache on Debian family
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Ensure service is running on production
service:
name: myapp
state: started
when: environment == "production"
Multiple Conditions (AND Logic)
Use a list to combine multiple conditions with AND logic (all must be true):
---
- name: Multiple AND conditions
hosts: webservers
tasks:
- name: Deploy only on Ubuntu 20.04 production servers
copy:
src: app.conf
dest: /etc/app/app.conf
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version == "20.04"
- environment == "production"
- app_deployed is defined
- name: Install security updates on critical systems
apt:
upgrade: security
when:
- ansible_os_family == "Debian"
- security_level == "critical"
- maintenance_window | default(false)
OR Logic and Complex Conditions
Use parentheses and boolean operators for OR logic and complex conditions:
---
- name: OR and complex conditional logic
hosts: all
tasks:
- name: Install package on Red Hat or CentOS
yum:
name: nginx
state: present
when: ansible_distribution == "RedHat" or ansible_distribution == "CentOS"
- name: Complex multi-line condition
service:
name: nginx
state: restarted
when: >
(ansible_distribution == "Ubuntu" and
ansible_distribution_version is version('20.04', '>='))
or
(ansible_distribution == "CentOS" and
ansible_distribution_major_version == "8")
- name: Install on multiple distributions
package:
name: curl
state: present
when: ansible_distribution in ["Ubuntu", "Debian", "RedHat", "CentOS"]
Testing Variables and Facts
Check if variables are defined, undefined, true, false, or match patterns:
---
- name: Variable testing conditions
hosts: all
vars:
app_port: 8080
enable_ssl: true
db_password: "secretpass"
tasks:
- name: Run only if variable is defined
debug:
msg: "Database password is set"
when: db_password is defined
- name: Run only if variable is undefined
fail:
msg: "Critical variable missing!"
when: api_key is not defined
- name: Check if variable is true
include_tasks: ssl_setup.yml
when: enable_ssl
- name: Check if variable is false
debug:
msg: "SSL is disabled"
when: not enable_ssl
- name: Check if string contains substring
debug:
msg: "This is a production server"
when: "'prod' in inventory_hostname"
- name: Check if list contains item
debug:
msg: "Port 443 is open"
when: 443 in open_ports
- name: Check variable type
debug:
msg: "Port is correctly configured"
when: app_port is number
- name: Version comparison
apt:
name: nginx
state: latest
when: nginx_version is version('1.18', '<')
- name: String matching with regex
debug:
msg: "Valid email format"
when: user_email is match("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
Conditionals with Registered Variables
Use output from previous tasks to make decisions:
---
- name: Using registered variables in conditionals
hosts: all
tasks:
- name: Check if configuration file exists
stat:
path: /etc/myapp/config.yml
register: config_file
- name: Create config from template if missing
template:
src: config.yml.j2
dest: /etc/myapp/config.yml
when: not config_file.stat.exists
- name: Check service status
command: systemctl is-active myapp
register: service_status
ignore_errors: yes
- name: Start service if not running
service:
name: myapp
state: started
when: service_status.rc != 0
- name: Get disk usage
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
- name: Warn if disk usage is high
debug:
msg: "WARNING: Disk usage is {{ disk_usage.stdout }}%"
when: disk_usage.stdout | int > 80
- name: Check application health
uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
ignore_errors: yes
- name: Restart application if unhealthy
service:
name: myapp
state: restarted
when: health_check.status != 200
Loops in Ansible
Basic Loop with List
The modern loop keyword iterates over lists of items:
---
- name: Basic loop examples
hosts: localhost
tasks:
- name: Create multiple users
user:
name: "{{ item }}"
state: present
groups: users
loop:
- alice
- bob
- charlie
- dave
- name: Install multiple packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis-server
- git
- curl
- name: Create multiple directories
file:
path: "/opt/app/{{ item }}"
state: directory
mode: '0755'
loop:
- logs
- config
- data
- backups
Loop with Dictionaries
Loop over complex data structures with dictionaries:
---
- name: Loop with dictionaries
hosts: webservers
vars:
users:
- name: alice
uid: 1001
groups: ['wheel', 'developers']
shell: /bin/bash
- name: bob
uid: 1002
groups: ['users']
shell: /bin/zsh
- name: charlie
uid: 1003
groups: ['wheel', 'operators']
shell: /bin/bash
tasks:
- name: Create users with properties
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups | join(',') }}"
shell: "{{ item.shell }}"
state: present
loop: "{{ users }}"
- name: Configure application instances
template:
src: app-config.j2
dest: "/etc/myapp/{{ item.name }}.conf"
loop:
- { name: 'web', port: 8080, workers: 4 }
- { name: 'api', port: 8081, workers: 8 }
- { name: 'admin', port: 8082, workers: 2 }
- name: Set up virtual hosts
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.domain }}.conf"
loop:
- { domain: 'example.com', root: '/var/www/example', ssl: true }
- { domain: 'api.example.com', root: '/var/www/api', ssl: true }
- { domain: 'staging.example.com', root: '/var/www/staging', ssl: false }
Loop with Index
Access the loop index with loop_control:
---
- name: Loop with index
hosts: localhost
tasks:
- name: Create numbered backup directories
file:
path: "/backup/set-{{ index }}"
state: directory
loop:
- daily
- weekly
- monthly
loop_control:
index_var: index
- name: Display items with position
debug:
msg: "Item {{ index + 1 }} of {{ servers | length }}: {{ item }}"
loop: "{{ servers }}"
loop_control:
index_var: index
- name: Create configuration with priority
lineinfile:
path: /etc/app/servers.conf
line: "server{{ idx }}.priority={{ idx }}"
create: yes
loop: [100, 90, 80, 70]
loop_control:
index_var: idx
Nested Loops
Loop through multiple lists simultaneously:
---
- name: Nested loop examples
hosts: localhost
vars:
environments: ['dev', 'staging', 'prod']
services: ['web', 'api', 'worker']
tasks:
- name: Create directories for each environment and service
file:
path: "/opt/{{ item.0 }}/{{ item.1 }}"
state: directory
loop: "{{ environments | product(services) | list }}"
- name: Alternative nested loop syntax
debug:
msg: "Deploying {{ item.1 }} to {{ item.0 }}"
with_nested:
- "{{ environments }}"
- "{{ services }}"
- name: Configure firewall rules for multiple ports and protocols
firewalld:
port: "{{ item.0 }}/{{ item.1 }}"
permanent: yes
state: enabled
loop: "{{ [80, 443, 8080] | product(['tcp', 'udp']) | list }}"
- name: Create user groups across multiple servers
user:
name: "{{ item.1 }}"
groups: "{{ item.0.group }}"
delegate_to: "{{ item.0.host }}"
loop: "{{ servers | subelements('users') }}"
vars:
servers:
- host: web01
group: webadmins
users: ['alice', 'bob']
- host: db01
group: dbadmins
users: ['charlie', 'dave']
Loop Until - Retry Logic
Repeat a task until a condition is met:
---
- name: Loop until examples
hosts: all
tasks:
- name: Wait for service to be ready
uri:
url: http://localhost:8080/health
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 10
- name: Wait for file to be created by another process
stat:
path: /var/run/app.ready
register: ready_file
until: ready_file.stat.exists
retries: 60
delay: 5
- name: Check for successful backup completion
shell: grep "Backup completed successfully" /var/log/backup.log
register: backup_status
until: backup_status.rc == 0
retries: 12
delay: 300 # Check every 5 minutes
- name: Wait for database to accept connections
postgresql_ping:
db: myapp
login_host: localhost
login_user: postgres
register: db_ping
until: db_ping is succeeded
retries: 20
delay: 15
- name: Poll for cluster quorum
command: rabbitmqctl cluster_status
register: cluster_status
until: "'running_nodes' in cluster_status.stdout and cluster_status.stdout.count('rabbit@') >= 3"
retries: 40
delay: 10
Loop Control - Advanced Options
Fine-tune loop behavior with loop_control:
---
- name: Advanced loop control
hosts: webservers
tasks:
- name: Loop with custom variable name
debug:
msg: "Processing server {{ server.name }}"
loop:
- { name: 'web01', ip: '10.0.1.10' }
- { name: 'web02', ip: '10.0.1.11' }
loop_control:
loop_var: server # Instead of 'item'
- name: Add pause between iterations
service:
name: "{{ item }}"
state: restarted
loop:
- app1
- app2
- app3
loop_control:
pause: 30 # Wait 30 seconds between restarts
- name: Custom loop labels for cleaner output
yum:
name: "{{ item }}"
state: present
loop:
- httpd-2.4.6-95.el7
- php-7.4.3-1.el7
- mariadb-server-10.3.27-3.el7
loop_control:
label: "{{ item.split('-')[0] }}" # Show only package name
- name: Nested loop with different variable names
file:
path: "/data/{{ env }}/{{ app }}"
state: directory
loop: "{{ ['dev', 'prod'] | product(['web', 'api']) | list }}"
loop_control:
loop_var: env_app
vars:
env: "{{ env_app[0] }}"
app: "{{ env_app[1] }}"
- name: Process large lists without flooding output
command: /usr/local/bin/process.sh {{ item }}
loop: "{{ range(1, 1001) | list }}"
loop_control:
label: "Processing item {{ item }}"
extended: no # Minimize output for large loops
Combining Conditionals and Loops
Conditional Execution Within Loops
Apply conditions to individual loop iterations:
---
- name: Conditionals in loops
hosts: all
vars:
packages:
- { name: 'nginx', os: 'all' }
- { name: 'apache2', os: 'Debian' }
- { name: 'httpd', os: 'RedHat' }
- { name: 'postgresql', os: 'all' }
- { name: 'docker.io', os: 'Debian' }
- { name: 'docker', os: 'RedHat' }
tasks:
- name: Install packages based on OS
package:
name: "{{ item.name }}"
state: present
loop: "{{ packages }}"
when: item.os == 'all' or item.os == ansible_os_family
- name: Create users, skipping system accounts
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
state: present
loop:
- { name: 'alice', uid: 1001 }
- { name: 'bob', uid: 1002 }
- { name: 'system', uid: 999 }
- { name: 'charlie', uid: 1003 }
when: item.uid >= 1000
- name: Deploy configurations only to production servers
template:
src: "{{ item }}.j2"
dest: "/etc/app/{{ item }}"
loop:
- database.conf
- cache.conf
- queue.conf
when: environment == "production"
- name: Start services on enabled hosts
service:
name: "{{ item.service }}"
state: started
loop:
- { service: 'nginx', enabled: true }
- { service: 'mysql', enabled: true }
- { service: 'redis', enabled: false }
- { service: 'memcached', enabled: false }
when: item.enabled
Complex Loop Filters
Use Jinja2 filters to transform and filter loop data:
---
- name: Advanced loop filtering
hosts: localhost
vars:
all_servers:
- { name: 'web01', role: 'web', status: 'active' }
- { name: 'web02', role: 'web', status: 'maintenance' }
- { name: 'db01', role: 'database', status: 'active' }
- { name: 'db02', role: 'database', status: 'active' }
- { name: 'cache01', role: 'cache', status: 'active' }
tasks:
- name: Process only active servers
debug:
msg: "{{ item.name }} is active"
loop: "{{ all_servers | selectattr('status', 'equalto', 'active') | list }}"
- name: Get all web servers
debug:
msg: "Web server: {{ item.name }}"
loop: "{{ all_servers | selectattr('role', 'equalto', 'web') | list }}"
- name: Process database servers that are active
debug:
msg: "Active DB: {{ item.name }}"
loop: "{{ all_servers | selectattr('role', 'equalto', 'database') | selectattr('status', 'equalto', 'active') | list }}"
- name: Get server names only
debug:
msg: "{{ item }}"
loop: "{{ all_servers | map(attribute='name') | list }}"
- name: Process unique roles
debug:
msg: "Role: {{ item }}"
loop: "{{ all_servers | map(attribute='role') | unique | list }}"
- name: Install on sorted list
debug:
msg: "Processing {{ item }}"
loop: "{{ ['nginx', 'apache', 'mysql', 'redis'] | sort }}"
Legacy Loop Constructs
with_items and with_dict
Legacy loop syntax (still supported but loop is preferred):
---
- name: Legacy loop syntax
hosts: localhost
tasks:
# with_items (equivalent to loop)
- name: Install packages with with_items
apt:
name: "{{ item }}"
state: present
with_items:
- vim
- git
- curl
# with_dict for looping over dictionaries
- name: Create users from dictionary
user:
name: "{{ item.key }}"
comment: "{{ item.value.name }}"
uid: "{{ item.value.uid }}"
with_dict:
alice:
name: "Alice Smith"
uid: 1001
bob:
name: "Bob Jones"
uid: 1002
# with_fileglob for file patterns
- name: Copy all configuration files
copy:
src: "{{ item }}"
dest: /etc/myapp/
with_fileglob:
- "configs/*.conf"
# with_together for parallel iteration
- name: Create users with specific UIDs
user:
name: "{{ item.0 }}"
uid: "{{ item.1 }}"
with_together:
- ['alice', 'bob', 'charlie']
- [1001, 1002, 1003]
# with_sequence for numeric ranges
- name: Create numbered directories
file:
path: "/backup/set-{{ item }}"
state: directory
with_sequence: start=1 end=10 stride=1
# Modern equivalent with loop
- name: Same with loop
file:
path: "/backup/set-{{ item }}"
state: directory
loop: "{{ range(1, 11) | list }}"
Best Practices
- Use
loop: Prefer modernloopsyntax over legacywith_*keywords - Keep conditions simple: Break complex conditions into multiple tasks for readability
- Use filters: Leverage Jinja2 filters to pre-process data before looping
- Label loops: Use
loop_control.labelto reduce output clutter - Avoid nested loops: Use Jinja2 filters like
product()instead - Handle failures: Use
ignore_errorsorfailed_whenwith loops - Use until wisely: Always set appropriate retries and delays
- Test conditions: Use
assertmodule to validate assumptions - Variable precedence: Understand which variables take priority in conditions
- Performance: Filter lists before looping to minimize iterations
Real-World Examples
Multi-Environment Deployment
---
- name: Environment-aware deployment
hosts: all
vars:
app_configs:
dev:
db_host: localhost
debug: true
workers: 2
staging:
db_host: staging-db.internal
debug: true
workers: 4
production:
db_host: prod-db.internal
debug: false
workers: 8
tasks:
- name: Deploy configuration for current environment
template:
src: app-config.j2
dest: /etc/app/config.yml
vars:
config: "{{ app_configs[environment] }}"
when: environment in app_configs
- name: Set up monitoring (production only)
include_role:
name: monitoring
when: environment == "production"
- name: Enable debug logging (non-production)
lineinfile:
path: /etc/app/config.yml
line: "debug: true"
when: environment in ["dev", "staging"]
Conditional Service Management
---
- name: Intelligent service management
hosts: all
tasks:
- name: Get service facts
service_facts:
- name: Restart services that need it
service:
name: "{{ item.name }}"
state: restarted
loop:
- { name: 'nginx', config: '/etc/nginx/nginx.conf' }
- { name: 'postgresql', config: '/etc/postgresql/12/main/postgresql.conf' }
- { name: 'redis', config: '/etc/redis/redis.conf' }
when:
- item.name in ansible_facts.services
- ansible_facts.services[item.name + '.service'].state == "running"
register: service_restart
- name: Verify service health after restart
uri:
url: "http://localhost/health"
status_code: 200
when: service_restart is changed
retries: 5
delay: 10
Progressive Rollout with Loops
---
- name: Canary deployment with health checks
hosts: webservers
serial: 1
tasks:
- name: Deploy new version
copy:
src: app-v2.tar.gz
dest: /opt/app/
- name: Restart application
service:
name: myapp
state: restarted
- name: Wait for application startup
wait_for:
port: 8080
timeout: 60
- name: Run health checks
uri:
url: "http://{{ inventory_hostname }}:8080{{ item }}"
status_code: 200
loop:
- /health
- /api/status
- /metrics
register: health_results
until: health_results is succeeded
retries: 10
delay: 5
- name: Abort if health checks fail
fail:
msg: "Health checks failed on {{ inventory_hostname }}"
when: health_results is failed
- name: Monitor for errors
shell: tail -100 /var/log/myapp/error.log | grep -i error | wc -l
register: error_count
until: error_count.stdout | int < 5
retries: 6
delay: 10
- name: Continue to next host only if successful
debug:
msg: "{{ inventory_hostname }} deployment successful, proceeding..."
Common Patterns and Idioms
Default Values and Fallbacks
---
- name: Using defaults in conditions
hosts: all
tasks:
- name: Use variable with default
debug:
msg: "App port is {{ app_port | default(8080) }}"
- name: Conditional with default
service:
name: "{{ item }}"
state: started
loop: "{{ enabled_services | default(['nginx', 'postgresql']) }}"
- name: Multi-level fallback
debug:
msg: "{{ custom_config | default(environment_config | default(default_config)) }}"
Loop with Error Handling
---
- name: Robust loop error handling
hosts: all
tasks:
- name: Try to start services, continue on failure
service:
name: "{{ item }}"
state: started
loop:
- app1
- app2
- app3
register: service_results
ignore_errors: yes
- name: Report which services failed
debug:
msg: "Failed to start {{ item.item }}"
loop: "{{ service_results.results }}"
when: item is failed
- name: Fail if critical services didn't start
fail:
msg: "Critical service {{ item.item }} failed to start"
loop: "{{ service_results.results }}"
when:
- item is failed
- item.item in critical_services
Next Steps
- Learn about Handlers for efficient change notifications
- Explore Plugins for custom filters and lookups
- Master Roles for organizing complex conditionals
- Study Troubleshooting for debugging loop issues
- Practice in the Playground with interactive examples