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.
Evaluating Conditions Based on Popular Facts #
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 Name | Data Type | Example Values | Usage Description |
|---|---|---|---|
ansible_os_family | String | Debian, RedHat, Suse | Groups tasks by main Linux distribution family. |
ansible_distribution | String | Ubuntu, CentOS, Rocky | Determines specific actions for a particular distribution. |
ansible_distribution_major_version | String | 20, 22, 8, 9 | Checks the major release version (for example for RHEL 9-specific configuration). |
ansible_memtotal_mb | Integer | 2048, 16384 | Sets application memory allocation parameters based on physical server RAM. |
ansible_processor_vcpus | Integer | 2, 4, 16 | Determines the worker process count (like worker_processes in Nginx). |
ansible_virtualization_type | String | kvm, docker, virtualbox | Avoids installing certain kernel modules if running inside a container. |
ansible_architecture | String | x86_64, aarch64 | Selects 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:
looponly accepts a flat one-dimensional list directly. This makes its behavior very predictable. - Declarative: You can combine
loopwith various Jinja2 filters (likeflatten,dict2items, orsubelements) 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
whencondition 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 insidewhen.
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 thewhenoption. Ansible conditional statements evaluate variables directly.- Facts Variables: Leverage system facts tables like
ansible_os_familyoransible_memtotal_mbto 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
loopkeyword instead of the oldwith_itemsfor future compatibility and syntax neatness.- Output Labels: Use
loop_control.labelto limit terminal execution log length when looping over dictionary lists.- Nested Looping: Use the
loop_control.loop_varoption on nested loops to avoiditemvariable namespace collisions.- Per-Item Evaluation: Attaching
wheninsidelooptriggers condition evaluation on each item one by one, not filtering or stopping the entire loop.