Conditionals Usage in Ansible Playbook

Last updated: June 29, 2025

My Journey with Conditionals in Ansible

When I first started with Ansible, my playbooks were pretty straightforward—install a package, copy some files, restart a service. But as my infrastructure grew more complex, I realized that I needed my automation to be smarter and more adaptable. That's when I discovered the power of conditionals in Ansible playbooks.

Learning to use conditionals effectively was a game-changer in my automation journey. Instead of writing separate playbooks for different environments or scenarios, I could create a single, intelligent playbook that could make decisions based on facts, variables, and previous task results. This approach dramatically reduced duplication in my code and made my automation much more maintainable.

In this blog post, I'll share my experience with using conditionals in Ansible playbooks, with practical examples for both Linux and Windows environments. Whether you're managing a handful of servers or a complex multi-platform infrastructure, mastering conditionals will take your Ansible automation to the next level.

Understanding Conditionals in Ansible

Conditionals in Ansible let you control the execution flow of your playbooks by making tasks run only when certain conditions are met. This is similar to if/elif/else statements in traditional programming languages, but with Ansible's declarative approach.

The most common way to implement conditionals in Ansible is through the when statement, which uses Jinja2 expressions to evaluate conditions. Unlike other places where you'd use Jinja2 syntax in Ansible, you typically don't need to surround expressions with {{ }} in a when statement.

Basic Conditionals with When

The when statement is the foundation of conditional execution in Ansible. Here's a simple example from my early days of using conditionals:

---
- name: Install appropriate web server based on OS
  hosts: all
  tasks:
    - name: Install Apache on Debian/Ubuntu systems
      apt:
        name: apache2
        state: present
      when: ansible_facts['os_family'] == "Debian"

    - name: Install Apache on RHEL/CentOS systems
      yum:
        name: httpd
        state: present
      when: ansible_facts['os_family'] == "RedHat"
      
    - name: Install IIS on Windows systems
      win_feature:
        name: Web-Server
        state: present
      when: ansible_facts['os_family'] == "Windows"

In this example, the appropriate web server installation task runs only on systems that match the specified OS family. Tasks with conditions that evaluate to false are skipped completely.

Comparison Operators

Conditions in Ansible use standard comparison operators that you might recognize from other programming languages:

  • == - Equal to

  • != - Not equal to

  • > - Greater than

  • < - Less than

  • >= - Greater than or equal to

  • <= - Less than or equal to

Here's an example checking disk space before performing a large file operation:

- name: Check available disk space
  shell: df -h / | grep -v Filesystem | awk '{print $5}' | sed 's/%//'
  register: disk_space
  changed_when: false  # This is just a check, no change should be recorded

- name: Perform large file operation
  ansible.builtin.copy:
    src: /path/to/large/file
    dest: /destination/path
  when: disk_space.stdout|int < 80  # Only run if disk is less than 80% full

Conditionals Based on Ansible Facts

One of the most powerful uses of conditionals is with Ansible facts. Facts are pieces of information about your managed hosts that Ansible gathers automatically when running playbooks.

Here's an example from a cross-platform automation project I worked on:

---
- name: Configure appropriate firewall based on OS
  hosts: all
  tasks:
    - name: Configure UFW on Ubuntu
      block:
        - name: Ensure UFW is installed
          apt:
            name: ufw
            state: present
            
        - name: Allow HTTP traffic
          ufw:
            rule: allow
            port: 80
            proto: tcp
      when: ansible_facts['distribution'] == "Ubuntu"
      
    - name: Configure firewalld on CentOS/RHEL 7+
      block:
        - name: Ensure firewalld is installed
          yum:
            name: firewalld
            state: present
            
        - name: Allow HTTP traffic
          firewalld:
            service: http
            permanent: yes
            state: enabled
      when: 
        - ansible_facts['distribution'] == "CentOS" or ansible_facts['distribution'] == "RedHat"
        - ansible_facts['distribution_major_version'] | int >= 7
        
    - name: Configure Windows Firewall
      win_firewall_rule:
        name: Allow HTTP
        localport: 80
        action: allow
        direction: in
        protocol: tcp
        state: present
        enabled: yes
      when: ansible_facts['os_family'] == "Windows"

Multiple Conditions

You can use logical operators to combine multiple conditions:

  • and - Both conditions must be true

  • or - Either condition can be true

  • not - Inverts the condition

For complex conditions, you can use parentheses to group expressions or use a list format for improved readability:

# Using parentheses for grouping
- name: Task with complex condition
  debug:
    msg: "This will run on CentOS 7+ or any Ubuntu"
  when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] | int >= 7) or ansible_facts['distribution'] == "Ubuntu"

# Using list format (all items are ANDed together)
- name: Task with list conditions
  debug:
    msg: "This will run only on CentOS 8"
  when:
    - ansible_facts['distribution'] == "CentOS"
    - ansible_facts['distribution_major_version'] | int == 8

Conditionals Based on Registered Variables

Another powerful way to use conditionals is with the register directive, which lets you capture the output from one task and use it to make decisions in subsequent tasks.

Here's a real-world example I use to check if a service is running before attempting to restart it:

---
- name: Manage application service
  hosts: app_servers
  tasks:
    - name: Check if service exists
      command: systemctl status my_application
      register: service_status
      ignore_errors: yes
      changed_when: false

    - name: Install service if it doesn't exist
      include_tasks: install_service.yml
      when: service_status.rc != 0

    - name: Restart service if it exists
      systemd:
        name: my_application
        state: restarted
      when: service_status.rc == 0

In Windows environments, I often use PowerShell to gather information that influences subsequent tasks:

---
- name: Manage Windows features
  hosts: windows_servers
  tasks:
    - name: Check if IIS is installed
      win_shell: Get-WindowsFeature -Name Web-Server | Select-Object -ExpandProperty InstallState
      register: iis_status
      changed_when: false
      
    - name: Display installation status
      debug:
        msg: "IIS is already installed"
      when: iis_status.stdout | trim == "Installed"
      
    - name: Install IIS if not present
      win_feature:
        name: Web-Server
        state: present
        include_management_tools: yes
      when: iis_status.stdout | trim != "Installed"

Testing for Success, Failure, or Change

Ansible provides special tests for registered variables:

  • when: result is success - Task ran successfully (return code 0)

  • when: result is failed - Task failed (return code not 0)

  • when: result is changed - Task made changes

  • when: result is skipped - Task was skipped

Here's how I use these in my playbooks:

- name: Try to create a database
  mysql_db:
    name: my_app_db
    state: present
  register: db_creation
  ignore_errors: yes
  
- name: Handle successful creation
  debug:
    msg: "Database was successfully created"
  when: db_creation is success

- name: Handle failed creation - maybe create user first
  include_tasks: create_db_user.yml
  when: db_creation is failed

Conditionals Based on Variables

You can also use variables defined in your inventory, playbooks, or vars files as the basis for conditionals.

Boolean Variables

Boolean variables make for clean, readable conditionals:

---
- name: Configure environment based on variables
  hosts: all
  vars:
    enable_monitoring: true
    enable_backups: false
    production_environment: true
    
  tasks:
    - name: Install monitoring agent
      include_tasks: install_monitoring.yml
      when: enable_monitoring
      
    - name: Configure backup agent
      include_tasks: setup_backup.yml
      when: enable_backups
      
    - name: Apply production hardening
      include_tasks: harden_system.yml
      when: production_environment

Testing for Variable Definition

Sometimes you need to check if a variable is defined before using it:

- name: Install custom package if specified
  package:
    name: "{{ custom_package }}"
    state: present
  when: custom_package is defined
  
- name: Use default package if custom not specified
  package:
    name: default-package
    state: present
  when: custom_package is not defined

This pattern is particularly useful when you have optional configurations or when you're building playbooks that can be customized via extra vars.

Advanced Conditional Techniques

Over time, I've developed some more sophisticated conditional techniques that have proven invaluable in complex automation scenarios.

Using Blocks with Conditionals

The block directive lets you apply a condition to multiple tasks at once, which can make your playbooks more readable and maintainable:

- name: Database server configuration
  hosts: db_servers
  tasks:
    - block:
        - name: Install MySQL
          package:
            name: mysql-server
            state: present
            
        - name: Start MySQL service
          service:
            name: mysqld
            state: started
            enabled: yes
            
        - name: Configure MySQL settings
          template:
            src: mysql.cnf.j2
            dest: /etc/my.cnf
      when: db_type == "mysql"
      
    - block:
        - name: Install PostgreSQL
          package:
            name: postgresql-server
            state: present
            
        - name: Initialize PostgreSQL database
          command: postgresql-setup initdb
          args:
            creates: /var/lib/pgsql/data/pg_hba.conf
            
        - name: Start PostgreSQL service
          service:
            name: postgresql
            state: started
            enabled: yes
      when: db_type == "postgresql"

Error Handling with Rescue and When

You can combine conditionals with Ansible's error handling features for more robust playbooks:

- name: Attempt critical operation with fallback
  block:
    - name: Try primary method
      shell: /usr/local/bin/critical_operation.sh
      register: primary_result
      
  rescue:
    - name: Try secondary method if primary fails
      shell: /usr/local/bin/fallback_operation.sh
      when: primary_result is defined and primary_result is failed
      
  when: perform_critical_operation | bool

This pattern has saved me many times in production environments where operations might fail and need alternative approaches.

Sequence Diagram: Conditional Execution Flow

To visualize how conditionals affect the execution flow in Ansible playbooks, I've created this sequence diagram:

This diagram illustrates how Ansible evaluates conditions before executing each task, and how the results of previous tasks can influence subsequent conditional evaluations.

Best Practices for Using Conditionals

Through my experience with Ansible, I've developed these best practices for working with conditionals:

1. Keep Conditions Simple and Readable

Break complex conditions into smaller, more manageable pieces:

# Instead of this:
when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] | int >= 7) or (ansible_facts['distribution'] == "Ubuntu" and ansible_facts['distribution_major_version'] | int >= 18)

# Do this:
when:
  - ansible_facts['distribution'] in ["CentOS", "Ubuntu"]
  - (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] | int >= 7) or
    (ansible_facts['distribution'] == "Ubuntu" and ansible_facts['distribution_major_version'] | int >= 18)

Group related tasks under a single condition using blocks:

- block:
    - name: Task 1
      # task details...
      
    - name: Task 2
      # task details...
      
    - name: Task 3
      # task details...
  when: some_condition

3. Consider Variable Types

Be mindful of variable types, especially when comparing numbers:

# This might not work as expected if version is a string
when: version > 7

# Better approach
when: version | int > 7

4. Test Conditions Before Deployment

Use ansible-playbook --check with liberal debug statements to verify your conditions are evaluating as expected:

- name: Debug condition evaluation
  debug:
    msg: "Condition evaluated to True"
  when: complex_condition
  tags: always

Real-World Examples

Here are some examples from my actual automation projects that demonstrate the power of conditionals in Ansible:

Cross-Platform Package Installation

---
- name: Install required packages across platforms
  hosts: all
  tasks:
    - name: Install packages on Debian/Ubuntu
      apt:
        name: "{{ debian_packages }}"
        state: present
        update_cache: yes
      when: ansible_facts['os_family'] == "Debian"
      vars:
        debian_packages:
          - curl
          - vim
          - htop
          - iotop

    - name: Install packages on RHEL/CentOS
      yum:
        name: "{{ rhel_packages }}"
        state: present
      when: ansible_facts['os_family'] == "RedHat"
      vars:
        rhel_packages:
          - curl
          - vim-enhanced
          - htop
          - iotop

    - name: Install packages on Windows
      win_chocolatey:
        name: "{{ windows_packages }}"
        state: present
      when: ansible_facts['os_family'] == "Windows"
      vars:
        windows_packages:
          - curl
          - vim
          - procexp

Environment-Specific Configuration

---
- name: Deploy application with environment-specific config
  hosts: app_servers
  vars:
    environments:
      development:
        db_host: dev-db.example.com
        log_level: debug
        feature_flags:
          new_ui: true
          analytics: false
      production:
        db_host: prod-db.example.com
        log_level: info
        feature_flags:
          new_ui: false
          analytics: true
          
  tasks:
    - name: Set environment-specific variables
      set_fact:
        db_host: "{{ environments[deploy_environment].db_host }}"
        log_level: "{{ environments[deploy_environment].log_level }}"
        feature_flags: "{{ environments[deploy_environment].feature_flags }}"
      when: deploy_environment in environments
      
    - name: Deploy application configuration
      template:
        src: app-config.json.j2
        dest: /opt/myapp/config.json
      when: deploy_environment in environments
      
    - name: Enable feature flags based on environment
      debug:
        msg: "New UI is {{ 'enabled' if feature_flags.new_ui else 'disabled' }}"
      when: feature_flags is defined and feature_flags.new_ui is defined

Self-Healing Infrastructure

One of my favorite uses of conditionals is creating self-healing infrastructure:

---
- name: Self-healing application deployment
  hosts: app_servers
  tasks:
    - name: Check application health
      uri:
        url: http://localhost:8080/health
        status_code: 200
      register: health_check
      ignore_errors: yes
      
    - name: Backup database before recovery
      mysql_db:
        name: myapp
        state: dump
        target: /backup/myapp-{{ ansible_date_time.iso8601 }}.sql
      when: health_check is failed
      
    - name: Attempt application restart
      systemd:
        name: myapp
        state: restarted
      when: health_check is failed
      register: restart_attempt
      
    - name: Restore from latest backup if restart fails
      block:
        - name: Find latest backup
          find:
            paths: /backup
            patterns: 'myapp-*.sql'
            sort: 'mtime'
          register: backups
          
        - name: Restore database from backup
          mysql_db:
            name: myapp
            state: import
            target: "{{ backups.files[-1].path }}"
          when: backups.matched > 0
      when: restart_attempt is failed or restart_attempt is skipped

Conclusion

Mastering conditionals has transformed how I approach Ansible automation. Instead of creating separate playbooks for different scenarios, I now build intelligent, adaptive playbooks that respond to the actual state of my infrastructure.

Conditionals make Ansible playbooks more powerful, flexible, and maintainable. They allow you to handle edge cases gracefully, adapt to different environments, and implement sophisticated automation logic without leaving the comfort of Ansible's declarative approach.

As you build your own automation, I encourage you to experiment with conditionals. Start simple, and as you gain confidence, you'll find yourself handling increasingly complex scenarios with elegant, readable code. The path to truly powerful automation is through smart, conditional execution.

Last updated