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
{% %}- Jinja2 statement syntax - for loops, if/else, and other logic.{{ }}- Jinja2 expression syntax - print this value.- Whitespace control -
{%- -%}trims the surrounding newlines a statement would otherwise leave. groups- the magic variable holding every group and its member hosts.hostvars- the magic variable giving access to another host's variables and facts.
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, pickansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.