Condition & Loop #

Smart infrastructure automation shouldn’t run rigidly and linearly. In real production environments, you’ll always face variations in target server conditions. You might need to install a particular software package only on CentOS, skip the database creation task if the database service isn’t installed yet, or repeat installing a long list of libraries on web servers without writing the same code lines over and over.

To handle this dynamic, Ansible provides two very important flow control features: conditionals (when) and iteration (loop). Understanding how to combine Jinja2 conditional logic evaluation with advanced loop control gives you full flexibility to design adaptive, safe, and efficient playbooks. This article breaks down conditional syntax, walks through modern iteration parameters, and digs into loop output optimization techniques.


Conditional Logic with when #

The when keyword determines whether Ansible runs or skips a task for a particular host. The condition written inside when is evaluated as a Jinja2 expression returning a boolean value (true or false).

The Golden Rule: No Curly Braces #

One of the most common mistakes made by beginners (and even experienced developers) is writing double curly braces {{ }} inside the when statement.

Remember this basic rule: when statements are already inside the Jinja2 evaluation environment implicitly. Adding {{ }} inside when is an anti-pattern that triggers syntax parser errors or unexpected variable evaluation behavior on modern Ansible versions.

Let’s compare the wrong and correct writing examples below:

# ANTI-PATTERN: Writing double curly braces inside when
- name: Install the utility package (Wrong Way)
  apt:
    name: curl
    state: present
  when: "{{ ansible_os_family == 'Debian' }}"
  # ✗ Ansible evaluates the curly braces twice,
  # which can trigger parser errors on Ansible 2.10 and above.
# CORRECT: Writing the conditional expression cleanly without curly braces
- name: Install the utility package (Correct Way)
  apt:
    name: curl
    state: present
  when: ansible_os_family == 'Debian'
  # ✓ The variable is read directly because the when module evaluates the string as a native Jinja2 expression.

When designing conditional expressions, you often rely on system characteristic data automatically collected by Ansible at the start of a play through the gathering facts process.

Here’s a reference table of popular facts variables often used as condition parameters in production environments:

Facts Variable NameData TypeExample ValuesUsage Description
ansible_os_familyStringDebian, RedHat, SuseGroups tasks by main Linux distribution family.
ansible_distributionStringUbuntu, CentOS, RockyDetermines specific actions for a particular distribution.
ansible_distribution_major_versionString20, 22, 8, 9Checks the major release version (for example for RHEL 9-specific configuration).
ansible_memtotal_mbInteger2048, 16384Sets application memory allocation parameters based on physical server RAM.
ansible_processor_vcpusInteger2, 4, 16Determines the worker process count (like worker_processes in Nginx).
ansible_virtualization_typeStringkvm, docker, virtualboxAvoids installing certain kernel modules if running inside a container.
ansible_architectureStringx86_64, aarch64Selects the right application binary package architecture (like ARM vs Intel).

Here’s an example of implementing conditionals using the facts table above to optimize kernel and package configuration:

# Set swap memory allocation only for low-spec servers
- name: Create an additional swap file
  command: /usr/local/bin/create_swap.sh
  when: 
    - ansible_memtotal_mb < 4096
    - ansible_virtualization_type != 'docker'
  # ✓ Swap won't be created if RAM >= 4GB or if the server runs inside a Docker container.

Writing Complex Conditions: AND, OR, and NOT #

Ansible supports standard logical operators for combining multiple conditional evaluation criteria.

1. AND Logic (All Conditions Must Be Met) #

There are two ways to write AND logic inside when. You can use the explicit and operator in one line, or write it as a YAML list format. The YAML list format is highly recommended because it makes your code lines much neater and easier to read.

# YAML List Format for AND Logic (Highly Recommended)
- name: Configure the Intel virtualization-specific kernel module
  modprobe:
    name: kvm_intel
    state: present
  when:
    - ansible_os_family == 'RedHat'
    - ansible_architecture == 'x86_64'
    - ansible_virtualization_type == 'kvm'
  # ✓ All three conditions above must be true for this task to run.

2. OR Logic (Either Condition Met) #

To use OR logic, write the or operator inside the conditional expression line.

# Using the OR operator
- name: Install additional network utilities
  apt:
    name: net-tools
    state: present
  when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian'
  # ✓ The task runs if the distribution is Ubuntu, Debian, or both.

3. NOT Logic (Negation/Inverse) #

You can use the not operator to invert the boolean evaluation result.

# Using the NOT operator
- name: Remove temporary files for non-production servers
  file:
    path: /tmp/debug_logs.txt
    state: absent
  when: not (env == 'production')
  # ✓ The file is only deleted if the env variable is NOT 'production'.

4. Writing Complex Multi-line Conditions #

If you must combine AND and OR operators with long parenthesis grouping, use the YAML scalar block indicator when: > to keep the code neat without horizontal scrolling:

# Scalar block for multi-line conditions
- name: Apply a critical security patch
  apt:
    name: security-patch
    state: latest
  when: >
    (ansible_distribution == 'Ubuntu' and ansible_distribution_version == '22.04')
    or
    (ansible_os_family == 'RedHat' and ansible_distribution_major_version | int >= 8)    

Modern Iteration: loop vs with_items (Legacy) #

Before Ansible version 2.5 was released, developers used various with_-prefixed keyword variants (like with_items, with_dict, with_subelements) for looping. Since version 2.5 and above, Ansible unified all those loop functions into one modern keyword: loop.

Although with_items is still supported for backward compatibility, using with_items in new projects is categorized as inappropriate (legacy/deprecated).

Why Choose loop? #

  • Simple: loop only accepts a flat one-dimensional list directly. This makes its behavior very predictable.
  • Declarative: You can combine loop with various Jinja2 filters (like flatten, dict2items, or subelements) to manipulate data structures before iteration. This cleanly separates loop logic from data processing.

Let’s look at the code writing comparison:

# LEGACY (with_items): Old style that's not recommended
- name: Create several project directories (Old Style)
  file:
    path: "{{ item }}"
    state: directory
  with_items:
    - /var/www/app1
    - /var/www/app2

# MODERN (loop): Highly recommended new style
- name: Create several project directories (Modern Style)
  file:
    path: "{{ item }}"
    state: directory
  loop:
    - /var/www/app1
    - /var/www/app2

Controlling Iteration Output with loop_control #

When you loop over a list containing complex data structures like dictionaries, the terminal screen output (stdout) displays the entire dictionary contents for every iteration. This makes your playbook execution logs very long, dirty, and hard to read.

Ansible provides the loop_control parameter to control the visual and functional behavior of your iteration process.

1. Tidying Output with label #

Use the label option to determine what concise dictionary property information you want displayed on the terminal stdout.

# Using loop_control to filter terminal log visualization
- name: Create system user accounts with complete parameters
  user:
    name: "{{ item.username }}"
    uid: "{{ item.uid }}"
    shell: "{{ item.shell }}"
    state: present
  loop:
    - { username: 'john', uid: 2001, shell: '/bin/bash', comment: 'Lead Developer' }
    - { username: 'jane', uid: 2002, shell: '/bin/zsh', comment: 'SecOps Engineer' }
  loop_control:
    label: "{{ item.username }} (UID: {{ item.uid }})"
  # ✓ Terminal output only shows: item=john (UID: 2001) and item=jane (UID: 2002).
  # Other sensitive or long property details won't clutter the screen.

2. Tracking the Loop Index with index_var #

Sometimes, inside an iteration you need a sequence index number (starting from 0). You can register an index holder variable using the index_var option.

# Tracking the row index for file numbering
- name: Copy configurations with unique numbering
  template:
    src: virtualhost.conf.j2
    dest: "/etc/nginx/sites-enabled/{{ idx + 1 }}-{{ item }}.conf"
  loop:
    - frontend
    - backend
    - api
  loop_control:
    index_var: idx
  # ✓ The generated files get a sequential number prefix: 1-frontend.conf, 2-backend.conf, etc.

3. Avoiding Namespace Collisions with loop_var (Nested Loops) #

When you write nested loops using include_tasks, the built-in item variable from the outer loop gets overwritten by the item variable in the inner loop. This causes variable read errors.

To prevent this collision, you must rename the iteration variable using the loop_var option.

# ==============================================================================
# FILE: main.yml (Main Playbook)
# ==============================================================================
- name: Configure virtual hosts per port
  include_tasks: configure_ports.yml
  loop:
    - { domain: 'app.example.com', directory: '/var/www/app' }
    - { domain: 'api.example.com', directory: '/var/www/api' }
  loop_control:
    loop_var: site_item
  # ✓ We rename the default 'item' variable to 'site_item' for the first-level loop.
# ==============================================================================
# FILE: configure_ports.yml (Included tasks)
# ==============================================================================
- name: Enable the listen port for domain {{ site_item.domain }}
  ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - 80
    - 443
  # ✓ Here we safely use the built-in 'item' variable for the inner loop
  # without worrying about disturbing the 'site_item' variable from the outer loop.

Combination: Evaluating when inside loop #

One scenario you’ll use very often is combining the when statement with the loop statement. When both keywords are attached to the same task, it’s important to understand Ansible’s internal evaluation flow.

[!IMPORTANT] Ansible evaluates the when condition for every item in the loop individually (one by one), not evaluating the loop as a whole. This means the loop still runs through all items, but the task only processes items that meet the criteria inside when.

Here’s a visualization of the per-item evaluation decision flow inside a conditional loop:

flowchart TD
    Start(["Start Loop Iteration"]) --> GetItem["Take Item X from the Loop List"]
    GetItem --> EvalCondition{"Evaluate the 'when' Condition against Item X"}
    
    EvalCondition -- "True" --> ExecTask["Run the Module Action for Item X"]
    ExecTask --> CheckNext{"Any Next Item?"}
    
    EvalCondition -- "False" --> SkipItem["Skip Item X"]
    SkipItem --> CheckNext
    
    CheckNext -- "Yes" --> GetItem
    CheckNext -- "No" --> EndLoop(["Loop Iteration Finished"])

Let’s look at the implementation example below. We want to create project directories, but only directories with the active: true property flag will be created.

# Per-item conditional application inside a loop
- name: Create active project directories
  file:
    path: "{{ item.path }}"
    state: directory
    owner: deployer
    group: deployer
    mode: '0755'
  loop:
    - { path: '/var/www/prod_site', active: true }
    - { path: '/var/www/staging_site', active: false }
    - { path: '/var/www/dev_site', active: true }
  when: item.active
  # ✓ Execution result: prod_site and dev_site directories are created,
  # while staging_site is skipped with a 'skipped' status.

Summary #

  • Jinja2 Without Curly Braces: Avoid writing {{ }} inside the when option. Ansible conditional statements evaluate variables directly.
  • Facts Variables: Leverage system facts tables like ansible_os_family or ansible_memtotal_mb to write adaptive conditions based on server specs.
  • Clean Logic: Use YAML list format for complex AND logic so your playbook code is easy to read vertically.
  • Loop Standardization: Use the modern loop keyword instead of the old with_items for future compatibility and syntax neatness.
  • Output Labels: Use loop_control.label to limit terminal execution log length when looping over dictionary lists.
  • Nested Looping: Use the loop_control.loop_var option on nested loops to avoid item variable namespace collisions.
  • Per-Item Evaluation: Attaching when inside loop triggers condition evaluation on each item one by one, not filtering or stopping the entire loop.

← Previous: Notify & Listen Next: Scope & Precedence →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact