Loops and Conditionals Inside Templates

Scenario: A load-balancer config lists eight backends. Adding a ninth server means editing the template, and someone forgets to add the trailing comma rules.

New words, in plain English

Templates can loop and branch, which is what lets one file describe a cluster whose membership changes.

{% for %} iterates. {% if %} branches. Crucially, both can read groups and hostvars, so a load balancer's config can be generated from the inventory itself - add a web server to the group, re-run, and the backend list updates. Nobody edits the template.

The practical wrinkle is whitespace. A bare {% for %} line leaves a blank line in the output where the statement was, and configuration formats that care about blank lines will notice. The - modifier trims it: {%- for x in y %} strips preceding whitespace, {% endfor -%} strips what follows. Getting this right is fiddly the first time and mechanical afterwards.

Keep the logic modest. A template with deeply nested conditionals is a program written in the worst available language, and it is far harder to test than the equivalent decision made in the playbook with a when.

Analogy: A template with a loop is a seating plan generated from the guest list rather than typed out by hand. Add a guest and the plan updates. Type the plan by hand and it is wrong the moment anything changes.

A worked example

# templates/backends.conf.j2 - generated FROM THE INVENTORY
# ANSIBLE MANAGED
upstream app_backends {
{% for host in groups['webservers'] %}
    server {{ hostvars[host]['ansible_host'] }}:{{ hostvars[host]['webapp_port'] | default(8080) }};
{% endfor %}
}

{% if app_env == 'production' %}
access_log /var/log/nginx/access.log combined;
{% else %}
access_log /var/log/nginx/access.log debug;
{% endif %}

# Whitespace control: no stray blank lines
{%- for cipher in tls_ciphers %}
ssl_cipher {{ cipher }};
{%- endfor %}

Reading another host's facts through hostvars requires those facts to have been gathered - either in an earlier play in the same run, or from a fact cache. A template that works when you run the whole playbook and fails when you run --tags lb alone is almost always this: the web servers' facts were never gathered in that narrower run.

When a template misbehaves, render it in isolation rather than guessing. A one-task playbook with template: writing to /tmp, run with --diff, shows exactly what the current variables produce - far faster than repeatedly deploying and inspecting.

Tip: Generate cluster membership from groups[...] rather than a hand-maintained list. It removes an entire category of stale-configuration incident.
Goal: Put this to work in the ansible-template-defaults lab. Open /labs/ansible, pick ansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.