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.