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.

Key Concepts:
  • Conditionals: Execute tasks based on specific criteria using when statements
  • Loops: Repeat tasks across multiple items with loop, with_*, or until
  • 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

Conditionals and Loops Best Practices:
  • Use loop: Prefer modern loop syntax over legacy with_* 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.label to reduce output clutter
  • Avoid nested loops: Use Jinja2 filters like product() instead
  • Handle failures: Use ignore_errors or failed_when with loops
  • Use until wisely: Always set appropriate retries and delays
  • Test conditions: Use assert module 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