Conditionals are essential for creating intelligent, adaptive Ansible playbooks that can make decisions based on facts, variables, and runtime conditions. This comprehensive guide covers all aspects of conditional execution in Ansible.
📑 Table of Contents
- 1. Basic When Clauses
- 2. OS and Distribution Checks
- 3. Multiple Conditions (AND/OR Logic)
- 4. Variable Defined/Undefined Checks
- 5. String Testing and Pattern Matching
- 6. List and Dictionary Tests
- 7. Numeric Comparisons
- 8. File and Path Tests
- 9. Registered Variable Conditions
- 10. failed_when and changed_when
- 11. Blocks with rescue and always
- 12. Complex Conditional Logic
- 13. Best Practices
- 1. Readability and Maintainability
- 2. Performance Optimization
- 3. Error Handling
- 4. Security Considerations
- 5. Testing Conditionals
- 6. Common Pitfalls to Avoid
- Conclusion
1. Basic When Clauses
The when
statement is the foundation of conditionals in Ansible. It evaluates a Jinja2 expression and executes the task only if the condition is true.
---
- name: Basic Conditional Examples
hosts: all
vars:
run_updates: true
environment: production
tasks:
- name: Update system packages
apt:
update_cache: yes
upgrade: dist
when: run_updates
- name: Install monitoring agent on production servers
package:
name: datadog-agent
state: present
when: environment == "production"
- name: Skip this task on test servers
debug:
msg: "This only runs on non-test servers"
when: environment != "test"
Key Points:
- Boolean variables don’t need comparison operators (just use
when: variable_name
) - String comparisons use
==
or!=
- Jinja2 expressions in
when
don’t need double curly braces - Tasks are skipped silently when conditions are false
2. OS and Distribution Checks
Ansible facts provide detailed information about target systems, making OS-specific tasks easy to implement.
---
- name: OS-Specific Package Installation
hosts: all
become: yes
tasks:
- name: Install Apache on Debian/Ubuntu
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install Apache on RedHat/CentOS
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Install Apache on Fedora
dnf:
name: httpd
state: present
when: ansible_distribution == "Fedora"
- name: Ubuntu-specific configuration
template:
src: ubuntu_config.j2
dest: /etc/app/config.conf
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version >= "20.04"
- name: CentOS 7 specific task
shell: systemctl enable legacy-service
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "7"
Common Ansible Facts for OS Checks:
ansible_os_family
: Debian, RedHat, Windows, Darwinansible_distribution
: Ubuntu, CentOS, Fedora, Debianansible_distribution_version
: 20.04, 8.5, etc.ansible_distribution_major_version
: 20, 8, 7ansible_system
: Linux, Win32NT, Darwinansible_architecture
: x86_64, aarch64, armv7l
3. Multiple Conditions (AND/OR Logic)
Combine multiple conditions using logical operators or list format for complex decision-making.
---
- name: Multiple Condition Examples
hosts: all
vars:
enable_ssl: true
environment: production
cpu_cores: 8
memory_gb: 16
tasks:
# AND condition - list format (all must be true)
- name: Deploy high-performance config
template:
src: high_perf_config.j2
dest: /etc/app/config.conf
when:
- cpu_cores >= 8
- memory_gb >= 16
- environment == "production"
# AND condition - inline format
- name: Enable SSL on production with certificate
service:
name: nginx-ssl
state: started
when: environment == "production" and enable_ssl
# OR condition - using parentheses
- name: Install on dev or staging
package:
name: debug-tools
state: present
when: environment == "development" or environment == "staging"
# Complex logic with AND/OR
- name: Complex conditional
debug:
msg: "Running complex condition"
when: >
(environment == "production" and enable_ssl) or
(environment == "staging" and cpu_cores >= 4)
# NOT logic
- name: Run only if NOT production
debug:
msg: "Safe to test here"
when: not environment == "production"
# IN operator
- name: Run on multiple environments
debug:
msg: "Running on allowed environment"
when: environment in ["development", "staging", "qa"]
4. Variable Defined/Undefined Checks
Check whether variables exist before using them to prevent errors.
---
- name: Variable Definition Checks
hosts: all
vars:
database_host: "db.example.com"
tasks:
- name: Run only if variable is defined
debug:
msg: "Database host is {{ database_host }}"
when: database_host is defined
- name: Set default if variable is undefined
set_fact:
app_port: 8080
when: app_port is not defined
- name: Check if variable is defined and not empty
debug:
msg: "API key is configured"
when:
- api_key is defined
- api_key | length > 0
- name: Use default_value filter
debug:
msg: "Port is {{ custom_port | default(8080) }}"
- name: Check if variable is None
debug:
msg: "Variable is explicitly set to None"
when: some_var is none
- name: Check if variable has a value (not None or undefined)
debug:
msg: "Variable has a value"
when: some_var is defined and some_var is not none
5. String Testing and Pattern Matching
Perform sophisticated string matching using regex, search, and match tests.
---
- name: String Testing Examples
hosts: all
vars:
hostname: "web-server-01.production.example.com"
service_name: "nginx-1.20.1"
log_level: "DEBUG"
tasks:
- name: Check if string starts with pattern (match)
debug:
msg: "This is a web server"
when: hostname is match("web-.*")
- name: Check if string contains pattern (search)
debug:
msg: "Running in production"
when: hostname is search("production")
- name: Regex test with groups
debug:
msg: "Nginx version detected"
when: service_name is regex("nginx-[0-9]+\.[0-9]+\.[0-9]+")
- name: Case-insensitive search
debug:
msg: "Debug logging enabled"
when: log_level | lower == "debug"
- name: Check string length
debug:
msg: "Long hostname detected"
when: hostname | length > 20
- name: Check if string is in list
debug:
msg: "Valid log level"
when: log_level in ["DEBUG", "INFO", "WARN", "ERROR"]
- name: String starts with
debug:
msg: "Production server"
when: hostname.startswith("web-server")
- name: String ends with
debug:
msg: "Example.com domain"
when: hostname.endswith(".example.com")
- name: Check if variable is a string
debug:
msg: "Variable is a string"
when: hostname is string
6. List and Dictionary Tests
Conditional logic for collection data types.
---
- name: List and Dictionary Conditionals
hosts: all
vars:
packages: ["nginx", "mysql", "redis"]
server_config:
ssl_enabled: true
port: 443
workers: 4
empty_list: []
tasks:
- name: Check if variable is a list
debug:
msg: "Packages is a list"
when: packages is iterable and packages is not string
- name: Check if list is not empty
debug:
msg: "We have packages to install"
when: packages | length > 0
- name: Check if item in list
debug:
msg: "Nginx will be installed"
when: "'nginx' in packages"
- name: Check if list is empty
debug:
msg: "No items in list"
when: empty_list | length == 0
- name: Check if variable is a dictionary
debug:
msg: "server_config is a dictionary"
when: server_config is mapping
- name: Check if key exists in dictionary
debug:
msg: "SSL is configured"
when: "'ssl_enabled' in server_config"
- name: Check dictionary value
debug:
msg: "SSL is enabled"
when:
- server_config.ssl_enabled is defined
- server_config.ssl_enabled
- name: Count dictionary keys
debug:
msg: "Config has {{ server_config | length }} settings"
when: server_config | length > 0
- name: Check if all items in list match condition
debug:
msg: "All ports are open"
when: server_config.port > 0
- name: Subset check
debug:
msg: "Required packages present"
when: "['nginx', 'mysql'] is subset(packages)"
7. Numeric Comparisons
Perform mathematical comparisons and numeric tests.
---
- name: Numeric Conditional Examples
hosts: all
vars:
available_memory: 16384
cpu_count: 8
disk_usage_percent: 75
app_version: "2.5.3"
tasks:
- name: Greater than comparison
debug:
msg: "High memory system"
when: available_memory > 8192
- name: Less than or equal
debug:
msg: "Disk usage acceptable"
when: disk_usage_percent <= 80
- name: Range check
debug:
msg: "CPU count in acceptable range"
when: cpu_count >= 4 and cpu_count <= 16
- name: Modulo operation
debug:
msg: "Even number of CPUs"
when: cpu_count % 2 == 0
- name: Check if number
debug:
msg: "Memory is numeric"
when: available_memory is number
- name: Check if integer
debug:
msg: "CPU count is integer"
when: cpu_count is integer
- name: Check if float
debug:
msg: "Value is float"
when: some_value is float
- name: Version comparison
debug:
msg: "App version is 2.5 or higher"
when: app_version is version('2.5', '>=')
- name: Version comparison with operator
debug:
msg: "Running latest major version"
when: app_version is version('2.0', '>=') and app_version is version('3.0', '<')
- name: Divisible by check
debug:
msg: "CPU count divisible by 4"
when: cpu_count is divisibleby(4)
8. File and Path Tests
Check file existence, type, and permissions using stat module with conditionals.
---
- name: File and Path Conditionals
hosts: all
tasks:
- name: Check if file exists
stat:
path: /etc/app/config.conf
register: config_file
- name: Create config if it doesn't exist
template:
src: config.conf.j2
dest: /etc/app/config.conf
when: not config_file.stat.exists
- name: Check if path is a directory
stat:
path: /var/www/html
register: web_dir
- name: Create directory if not exists
file:
path: /var/www/html
state: directory
when: not web_dir.stat.exists or not web_dir.stat.isdir
- name: Check file permissions
stat:
path: /etc/ssl/private/key.pem
register: ssl_key
- name: Fix permissions if needed
file:
path: /etc/ssl/private/key.pem
mode: '0600'
when:
- ssl_key.stat.exists
- ssl_key.stat.mode != '0600'
- name: Check if file is a symlink
stat:
path: /usr/bin/python
register: python_bin
- name: Report symlink info
debug:
msg: "Python is a symlink to {{ python_bin.stat.lnk_source }}"
when:
- python_bin.stat.exists
- python_bin.stat.islnk
- name: Check file age
stat:
path: /var/log/app.log
register: log_file
- name: Rotate old log file
command: mv /var/log/app.log /var/log/app.log.old
when:
- log_file.stat.exists
- log_file.stat.mtime < (ansible_date_time.epoch | int - 86400)
- name: Check file size
stat:
path: /var/cache/downloads/large_file.zip
register: download_file
- name: Remove if file too large
file:
path: /var/cache/downloads/large_file.zip
state: absent
when:
- download_file.stat.exists
- download_file.stat.size > 1073741824 # 1GB in bytes
9. Registered Variable Conditions
Use output from previous tasks to control subsequent task execution.
---
- name: Registered Variable Conditionals
hosts: all
tasks:
- name: Check if service is running
command: systemctl is-active nginx
register: nginx_status
failed_when: false
changed_when: false
- name: Start nginx if not running
service:
name: nginx
state: started
when: nginx_status.rc != 0
- name: Run application test
command: /opt/app/test.sh
register: test_result
ignore_errors: yes
- name: Deploy rollback if test failed
command: /opt/app/rollback.sh
when: test_result.rc != 0
- name: Get disk usage
shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
register: disk_usage
changed_when: false
- name: Send alert if disk usage high
debug:
msg: "WARNING: Disk usage is {{ disk_usage.stdout }}%"
when: disk_usage.stdout | int > 80
- name: Check for package
command: dpkg -l | grep nginx
register: package_check
failed_when: false
changed_when: false
- name: Install package if missing
apt:
name: nginx
state: present
when: package_check.rc != 0
- name: Query database
command: mysql -u root -e "SELECT COUNT(*) FROM users;"
register: user_count
changed_when: false
- name: Import sample data if empty
command: mysql -u root < /tmp/sample_data.sql
when: user_count.stdout_lines[1] | int == 0
- name: Get service status JSON
uri:
url: http://localhost:8080/health
return_content: yes
register: health_check
- name: Restart service if unhealthy
service:
name: myapp
state: restarted
when:
- health_check.status == 200
- health_check.json.status != "healthy"
10. failed_when and changed_when
Control when Ansible considers a task as failed or changed.
---
- name: failed_when and changed_when Examples
hosts: all
tasks:
# Custom failure conditions
- name: Check application status
command: /opt/app/status.sh
register: app_status
failed_when:
- app_status.rc != 0
- "'WARNING' not in app_status.stderr"
- name: Validate configuration
command: nginx -t
register: nginx_test
failed_when: "'test failed' in nginx_test.stderr"
- name: Check disk space
shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
register: disk_check
failed_when: disk_check.stdout | int > 90
changed_when: false
# Custom changed conditions
- name: Ensure line in config
lineinfile:
path: /etc/app/config.conf
line: "max_connections = 1000"
register: config_update
changed_when: "'changed' in config_update.msg"
- name: Run idempotent script
command: /opt/scripts/setup.sh
register: setup_result
changed_when: "'Configuration updated' in setup_result.stdout"
- name: Check and report only
command: systemctl status nginx
register: service_status
changed_when: false
failed_when: false
# Complex logic
- name: Database migration
command: /opt/app/migrate.sh
register: migration
changed_when: "'migrations applied' in migration.stdout"
failed_when:
- migration.rc != 0
- "'already up to date' not in migration.stdout"
- name: API health check
uri:
url: http://localhost:8080/health
status_code: [200, 503]
register: api_health
failed_when:
- api_health.status == 503
- "'maintenance' not in api_health.content"
changed_when: false
# Never fail
- name: Best effort cleanup
command: rm -f /tmp/old_cache/*
failed_when: false
# Always changed
- name: Force handler notification
command: /bin/true
changed_when: true
notify: Restart Application
handlers:
- name: Restart Application
service:
name: myapp
state: restarted
11. Blocks with rescue and always
Group tasks together with error handling using blocks, similar to try/catch/finally.
---
- name: Block, Rescue, and Always Examples
hosts: all
tasks:
- name: Deploy application with error handling
block:
- name: Stop application
service:
name: myapp
state: stopped
- name: Backup current version
command: cp -r /opt/app /opt/app.backup
- name: Deploy new version
unarchive:
src: /tmp/myapp-v2.tar.gz
dest: /opt/app
- name: Run database migrations
command: /opt/app/migrate.sh
- name: Start application
service:
name: myapp
state: started
- name: Health check
uri:
url: http://localhost:8080/health
status_code: 200
rescue:
- name: Log deployment failure
debug:
msg: "Deployment failed, rolling back..."
- name: Restore backup
command: rm -rf /opt/app && mv /opt/app.backup /opt/app
- name: Start old version
service:
name: myapp
state: started
- name: Send alert
debug:
msg: "ALERT: Deployment failed, rolled back to previous version"
always:
- name: Remove temporary files
file:
path: /tmp/myapp-v2.tar.gz
state: absent
- name: Log deployment attempt
lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - Deployment attempt completed"
create: yes
# Conditional blocks
- name: Production-only deployment block
block:
- name: Enable maintenance mode
command: /opt/app/maintenance.sh on
- name: Deploy to production
command: /opt/app/deploy.sh production
- name: Disable maintenance mode
command: /opt/app/maintenance.sh off
when: environment == "production"
# Nested blocks
- name: Complex deployment with nested error handling
block:
- name: Pre-deployment checks
block:
- name: Check disk space
command: df -h /opt
register: disk_space
- name: Fail if insufficient space
fail:
msg: "Insufficient disk space"
when: disk_space.stdout is search("9[0-9]%|100%")
rescue:
- name: Clean old deployments
command: /opt/scripts/cleanup.sh
- name: Actual deployment
command: /opt/app/deploy.sh
rescue:
- name: Emergency rollback
command: /opt/app/rollback.sh
12. Complex Conditional Logic
Real-world examples combining multiple conditional techniques.
---
- name: Complex Real-World Conditionals
hosts: all
vars:
environment: production
enable_monitoring: true
tasks:
# Multi-tier application deployment
- name: Determine server role
set_fact:
server_role: >-
{%- if 'webservers' in group_names -%}
web
{%- elif 'dbservers' in group_names -%}
database
{%- elif 'cacheservers' in group_names -%}
cache
{%- else -%}
unknown
{%- endif -%}
- name: Install role-specific packages
package:
name: "{{ item }}"
state: present
loop: "{{ packages[server_role] }}"
when:
- server_role in packages
- packages[server_role] is defined
- packages[server_role] | length > 0
vars:
packages:
web: ["nginx", "php-fpm"]
database: ["mysql-server"]
cache: ["redis-server"]
# Conditional based on multiple factors
- name: Configure application
template:
src: "app_{{ server_role }}_{{ environment }}.conf.j2"
dest: /etc/app/config.conf
when:
- server_role != "unknown"
- environment in ["development", "staging", "production"]
notify: Restart Application
# Maintenance window check
- name: Check if in maintenance window
set_fact:
in_maintenance_window: >-
{{
(ansible_date_time.hour | int >= 2 and ansible_date_time.hour | int < 4)
or
(ansible_date_time.weekday | int == 6)
}}
- name: Run heavy maintenance tasks
command: /opt/scripts/reindex_database.sh
when:
- in_maintenance_window
- environment == "production"
- "'dbservers' in group_names"
# Feature flag system
- name: Load feature flags
set_fact:
features:
new_ui: "{{ lookup('env', 'FEATURE_NEW_UI') | default('false') | bool }}"
beta_api: "{{ lookup('env', 'FEATURE_BETA_API') | default('false') | bool }}"
- name: Deploy new UI
copy:
src: new_ui/
dest: /var/www/html/
when:
- features.new_ui
- server_role == "web"
# Complex validation
- name: Validate SSL certificate
block:
- name: Get certificate info
command: openssl x509 -in /etc/ssl/certs/server.crt -noout -enddate
register: cert_info
changed_when: false
- name: Parse expiry date
set_fact:
cert_expiry: "{{ cert_info.stdout | regex_replace('notAfter=(.+)', '\1') }}"
- name: Check if certificate expiring soon
set_fact:
cert_expiring: >-
{{
(cert_expiry | to_datetime('%b %d %H:%M:%S %Y %Z')).epoch
<
(ansible_date_time.epoch | int + 2592000)
}}
- name: Renew certificate if needed
command: /opt/scripts/renew_cert.sh
when: cert_expiring
when:
- environment == "production"
- enable_ssl is defined
- enable_ssl
handlers:
- name: Restart Application
service:
name: "{{ 'nginx' if server_role == 'web' else 'myapp' }}"
state: restarted
when: server_role in ['web', 'database', 'cache']
13. Best Practices
1. Readability and Maintainability
# Bad - Hard to read
- name: Complex condition
debug: msg="test"
when: (var1 == "prod" and var2 > 10 and var3 is defined) or (var1 == "dev" and var4 | length > 0)
# Good - Use YAML list format for AND conditions
- name: Complex condition - readable
debug:
msg: "Production with high load"
when:
- environment == "production"
- cpu_usage > 10
- monitoring_enabled is defined
# Good - Use variables for complex logic
- name: Set deployment flag
set_fact:
should_deploy: >-
{{
(environment == 'production' and cpu_usage > 10 and monitoring_enabled is defined)
or
(environment == 'development' and debug_mode)
}}
- name: Deploy application
command: /opt/app/deploy.sh
when: should_deploy
2. Performance Optimization
# Bad - Runs on every host even if not needed
- name: Install on specific host
package:
name: special-package
state: present
when: inventory_hostname == "server-01"
# Good - Use limit or delegate_to
- name: Install on specific host
package:
name: special-package
state: present
delegate_to: server-01
run_once: true
# Good - Skip gathering facts if not needed
- name: Simple deployment
hosts: all
gather_facts: no
tasks:
- name: Copy file
copy:
src: app.conf
dest: /etc/app/
3. Error Handling
# Bad - Silent failures
- name: Optional task
command: /opt/scripts/optional.sh
when: some_condition
ignore_errors: yes
# Good - Explicit error handling
- name: Optional task with logging
command: /opt/scripts/optional.sh
register: optional_result
failed_when:
- optional_result.rc != 0
- optional_result.rc != 2 # 2 means "already done"
when: some_condition
- name: Log if failed
debug:
msg: "Optional task failed but continuing: {{ optional_result.stderr }}"
when:
- optional_result is defined
- optional_result.rc != 0
4. Security Considerations
# Bad - Exposing sensitive data in when clause
- name: Check password
debug:
msg: "Password check"
when: user_password == "secret123"
# Good - Use no_log and proper validation
- name: Validate credentials
command: /opt/scripts/validate_creds.sh
register: cred_check
no_log: true
failed_when: cred_check.rc != 0
- name: Proceed with authenticated task
command: /opt/scripts/deploy.sh
when: cred_check.rc == 0
no_log: true
5. Testing Conditionals
# Use assert module to test conditions
- name: Validate environment before deployment
assert:
that:
- environment in ["development", "staging", "production"]
- ansible_distribution in ["Ubuntu", "CentOS", "RedHat"]
- ansible_memory_mb.real.total >= 2048
fail_msg: "Pre-deployment validation failed"
success_msg: "Pre-deployment validation passed"
# Use debug with verbosity for testing
- name: Debug conditional logic
debug:
msg: "Would deploy: {{ should_deploy }}, Reason: {{ deploy_reason }}"
verbosity: 1
vars:
should_deploy: "{{ environment == 'production' and health_check.status == 200 }}"
deploy_reason: >-
Environment: {{ environment }},
Health: {{ health_check.status | default('unknown') }}
6. Common Pitfalls to Avoid
# Pitfall 1: Using when with include/import
# Bad - when applies to include, not tasks inside
- import_tasks: database.yml
when: setup_database
# Good - Use when inside the included file or use include_tasks
- include_tasks: database.yml
when: setup_database
# Pitfall 2: String/Boolean confusion
# Bad - String comparison
- name: Check boolean
debug: msg="enabled"
when: feature_enabled == "true"
# Good - Boolean comparison
- name: Check boolean
debug: msg="enabled"
when: feature_enabled | bool
# Pitfall 3: Undefined variable errors
# Bad - Will fail if var is undefined
- name: Use variable
debug: msg="{{ my_var }}"
when: my_var != ""
# Good - Check if defined first
- name: Use variable safely
debug: msg="{{ my_var }}"
when:
- my_var is defined
- my_var | length > 0
# Pitfall 4: Changed status in conditionals
# Bad - Task always runs after command
- name: Run command
command: /bin/true
register: result
- name: Run if changed
debug: msg="changed"
when: result is changed # command always shows changed
# Good - Use changed_when to control
- name: Run command
command: /bin/true
register: result
changed_when: false
- name: Run if changed
debug: msg="changed"
when: result is changed
Conclusion
Mastering Ansible conditionals enables you to create intelligent, adaptive playbooks that can handle complex scenarios across diverse infrastructure. Key takeaways:
- Start Simple: Use basic
when
clauses and gradually build complexity - Use Facts: Leverage ansible_facts for OS, hardware, and network information
- Test Thoroughly: Use
--check
mode andassert
to validate conditions - Handle Errors: Use
block/rescue/always
for robust error handling - Keep It Readable: Break complex conditions into variables and use YAML list format
- Be Explicit: Check for defined variables and use
failed_when/changed_when
appropriately
With these conditional techniques, your Ansible playbooks can intelligently adapt to different environments, handle edge cases gracefully, and provide robust automation across your infrastructure.
Next Steps: Learn about Ansible Variables and Precedence to master data flow in your playbooks.
Was this article helpful?