Red Hat Certified Engineer (RHCE) #9 Tags and conditionals: when, loop, until
If #8 Error handling gave you a firm grip on dealing with failure, this time we organize the flow control that decides when a task runs, how many times it repeats, and which parts to run selectively. You set conditions with when, iterate over lists with loop, retry until success with until, and run only part of a playbook with tags. These four are tools that show up in nearly every RHCE hands-on exam.
when: conditional execution #
when is the key that decides whether a task runs. If the condition is true it runs; if false it’s skipped. The value is a Jinja2 expression, but inside when you write variable names directly, without {{ }}.
- name: Install the Apache package
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_facts['os_family'] == "RedHat"On hosts where the condition is false, the task is shown as skipping and doesn’t affect idempotency.
Fact-based branching #
The most common pattern in the exam is branching that does different work depending on OS type or version. You use the values gathered into ansible_facts in the condition.
- name: Install apache2 on Debian-based systems
ansible.builtin.apt:
name: apache2
state: present
when: ansible_facts['os_family'] == "Debian"
- name: Run only on RHEL 9 or later
ansible.builtin.debug:
msg: "This is RHEL 9 or later"
when:
- ansible_facts['distribution'] == "RedHat"
- ansible_facts['distribution_major_version'] | int >= 9distribution_major_version is a string, so you convert it to an integer with the | int filter before comparing. Skip this conversion and the numeric comparison can go wrong.
Combining with and, or #
When you bundle multiple conditions there are two approaches. Listing them out means it runs when all conditions are true (and).
- name: When both conditions are true
ansible.builtin.debug:
msg: "Conditions met"
when:
- ansible_facts['os_family'] == "RedHat"
- ansible_facts['memtotal_mb'] > 2048You write or or compound logic as a single-line expression. Make precedence explicit with parentheses.
- name: When it's RHEL or Fedora
ansible.builtin.debug:
msg: "Red Hat family"
when: >
ansible_facts['distribution'] == "RedHat" or
ansible_facts['distribution'] == "Fedora"Checking whether a variable is defined #
Branching on whether a variable is defined is also frequent. You use is defined, is undefined, and is none.
- name: Install only when the extra_pkg variable is defined
ansible.builtin.dnf:
name: "{{ extra_pkg }}"
state: present
when: extra_pkg is definedWhen branching on a value’s truthiness, you either leave the variable name as is or use the | bool filter, as in when: enable_service | bool.
loop: iterating over lists #
loop runs a single task repeatedly over each item of a list. The current item in the iteration is referenced with the item variable.
- name: Install multiple packages
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop:
- httpd
- mariadb-server
- phpWhen a module takes a list directly — as with package installation — it’s more efficient to pass it all at once as name: "{{ packages }}", but loop shows its true worth when each item needs an independent call, as with user creation.
Bulk-creating a user list #
The exam staple of bulk-creating a user list is loop’s signature example. If you make each item a dict, you can access values inside the item with item.key.
- name: Create users in bulk
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: alice, groups: wheel }
- { name: bob, groups: developers }
- { name: carol, groups: developers }Putting the user list in a variable file and referencing it from loop keeps the body of the playbook clean.
- name: Create users defined as variables
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
state: present
loop: "{{ users }}"with_items is the old notation #
Older playbooks used with_items. The behavior is the same, but loop is the recommended notation now; write new playbooks with loop.
# Old notation (for reference)
- name: The with_items way
ansible.builtin.dnf:
name: "{{ item }}"
state: present
with_items: [httpd, vsftpd]with_items differs subtly from loop in that it flattens the list one level. As long as you don’t deal with nested lists the result is the same, so on the exam just use loop consistently.
Iterating a dict: dict2items #
To iterate over a dict (a set of key-value pairs) rather than a list, you convert it to a list with the dict2items filter. After conversion, each item has item.key and item.value.
- name: Iterate over a dict as items
ansible.builtin.debug:
msg: "{{ item.key }} = {{ item.value }}"
loop: "{{ user_roles | dict2items }}"If user_roles is a dict like the following, it iterates twice.
user_roles:
alice: admin
bob: developerloop_control: loop_var and label #
When loops nest through a role or an include, both the inner and outer loop use item, causing a collision. In that case you change the loop variable name with loop_var under loop_control.
- name: Change the loop variable name
ansible.builtin.user:
name: "{{ user_item.name }}"
state: present
loop: "{{ users }}"
loop_control:
loop_var: user_itemAlso, iterating over dict items prints the whole dict at length in the output, cluttering the log. Specifying only the value to display for each iteration with label keeps the output clean.
- name: Specify an output label
ansible.builtin.user:
name: "{{ item.name }}"
state: present
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}"until: retries #
until runs a task repeatedly until a condition becomes true. You use it to wait for an external service to come up, or to retry an operation that fails intermittently. You set the maximum number of attempts with retries and the interval between attempts (in seconds) with delay.
- name: Wait for the service to respond
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: result
until: result.status == 200
retries: 5
delay: 10The task above attempts up to 5 times at 10-second intervals until the response is 200. If the condition never becomes true in the end, the task fails. The key point is that to use until you must capture the result with register and reference it in the condition. When you use a module like command for a query purpose — one that’s caught as changed every time — set changed_when: false alongside it to keep idempotency (you can see this in the combined example below).
tags: partial runs #
tags attach a name label to a task or play, so you can selectively run only the parts you need rather than the whole playbook. It’s useful for quickly running just a specific piece of a long playbook.
- name: Install Apache
ansible.builtin.dnf:
name: httpd
state: present
tags:
- packages
- name: Deploy the Apache config
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
tags:
- config–tags and –skip-tags #
--tags runs only the tasks carrying the specified tag. --skip-tags, conversely, runs everything except the specified tag.
# Run only tasks tagged config
ansible-playbook site.yml --tags config
# Skip only the packages tag and run the rest
ansible-playbook site.yml --skip-tags packagesansible-navigator uses the same options.
ansible-navigator run site.yml -m stdout --tags configTo check which tags are defined, you use --list-tags.
ansible-playbook site.yml --list-tagsPlay-level tags #
If you tag an entire play, all tasks in that play inherit the tag.
- name: Configure the web server
hosts: web
tags:
- web
tasks:
- name: Install httpd
ansible.builtin.dnf:
name: httpd
state: presentalways and never #
A task tagged always always runs, even when you specify a different tag with --tags. You can still exclude it with --skip-tags always. Conversely, a task tagged never is not run by default, and runs only when you explicitly name that tag or another tag on the task.
- name: Pre-check (always runs)
ansible.builtin.debug:
msg: "Playbook starting"
tags:
- always
- name: Print debug info (only when specified)
ansible.builtin.debug:
var: ansible_facts
tags:
- never
- debugAbove, the debug task is skipped normally, but runs when you invoke it with --tags debug. It’s useful for keeping a heavy diagnostic task out of ordinary runs.
Combined example: OS branching + loop + retries #
Let’s gather everything so far into one playbook. We branch by OS family to install packages, bulk-create users, retry until the service is active, and tag each step.
---
- name: Configure the web server
hosts: web
become: true
vars:
web_users:
- { name: deploy, groups: wheel }
- { name: app, groups: web }
tasks:
- name: Startup notice (always runs)
ansible.builtin.debug:
msg: "Starting configuration"
tags:
- always
- name: Install RHEL-family packages
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_facts['os_family'] == "RedHat"
tags:
- packages
- name: Create operations users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop: "{{ web_users }}"
loop_control:
label: "{{ item.name }}"
tags:
- users
- name: Start the service
ansible.builtin.service:
name: httpd
state: started
enabled: true
tags:
- service
- name: Confirm the service is active
ansible.builtin.command: systemctl is-active httpd
register: httpd_state
until: httpd_state.stdout == "active"
retries: 5
delay: 3
changed_when: false
tags:
- serviceInvoking this playbook with --tags users runs only the startup notice (always) and user creation.
Exam points #
- when takes variable names without
{{ }}. In fact-based branching, comparisons likeansible_facts['os_family']anddistribution_major_version | intare staples. - Write and as a list, or as a single-line expression. A list runs only when all conditions are true.
- Use
is definedto check whether a variable exists. Add| boolto judge a value’s truthiness. - loop’s current item is
item, and you access a dict item asitem.nameand so on. Bulk-creating a user list is the signature exam pattern. - To iterate a dict, convert it with the
dict2itemsfilter and accessitem.key,item.value. - For nested loops, avoid variable collisions with
loop_control.loop_var, and tidy the output withlabel. - until pairs with
register. Set the number of retries and the interval withretriesanddelay. Addchanged_when: falseto a command used for a query. - Run partially with tags. Select with
--tags, exclude with--skip-tags, and check with--list-tags.alwaysruns always;neverruns only when specified.
Wrap-up #
What this post locked in:
- when. Conditional execution. Fact- and variable-based branching, and (list) and or (expression),
is defined - loop. List iteration. The
itemreference, bulk user creation,dict2items,loop_varandlabelunderloop_control - until. Retries. Waiting for a service to come up by combining
register,retries, anddelay - tags. Partial runs.
--tags,--skip-tags,--list-tags, play-level tags,alwaysandnever
These four tools keep reappearing in the RHCSA automation posts (#14〜#17) for OS branching and bulk list processing. Get them in hand and the later posts come much easier.
Next: Ansible Vault #
Flow control is locked in. Now we move on to handling sensitive values like passwords and API keys that end up in a playbook, safely.
In #10 Ansible Vault: managing secrets, we’ll organize hands-on the exam-staple patterns: encrypting variables with ansible-vault, running playbooks with --ask-vault-pass and a vault password file, and managing plaintext and ciphertext separately.