Whitespace Control and Mistakes
Jinja2 keeps the whitespace around its tags, so a {% for %} loop that looks clean in the .j2 renders a config full of blank lines — one per iteration, where the {% %} line used to be. The output is correct but ugly, and on some parsers ugly tips over into broken. The fix is trim markers ({%- and -%}) and the trim_blocks / lstrip_blocks settings, which strip the newline and leading space a control tag leaves behind.
This is also the topic that collects the templating footguns that bite once a file stops being trivial: how undefined variables behave, and why a variable that happens to contain {{ }} gets re-rendered as if it were a template. None of these are exotic. They are the everyday ways a working template starts producing noisy diffs or silent wrong output, and each has a clean fix once you know it is there.
The Blank-Line Problem
A {% for host in groups['web'] %} line, the matching {% endfor %}, and any {% if %} each leave their own newline in the output. Loop over five hosts and the rendered upstream block carries a scatter of blank lines through it, because every control-tag line that printed nothing still left behind the newline that followed it in the source. It is harmless to nginx but it makes diffs noisy, and stricter readers — pg_hba.conf among them — are less forgiving.
Trim Markers
{%- strips whitespace before the tag, and -%} strips it after. Writing the loop control lines as {% for ... -%} and {%- endfor %} removes the newlines they would otherwise emit, giving tight output without restructuring the template. The dashes are surgical — you put them exactly where a stray newline is coming from, and leave the rest of the tag alone.
{% for %}{%- … -%}# without trimming — one blank line per iteration {% for host in groups['web'] %} server {{ hostvars[host]['ansible_host'] }}:8000; {% endfor %} # with trim markers — tight output, no blank lines {% for host in groups['web'] -%} server {{ hostvars[host]['ansible_host'] }}:8000; {%- endfor %}
The danger is over-trimming. Put -%} on a line where you actually wanted the trailing newline and you collapse two directives onto one line, breaking the file just as thoroughly as the blank lines did. Trim markers cut both ways: under-trimming leaves noise, over-trimming fuses lines. The cure for both is to look at the rendered output, not the template, when deciding where a dash belongs.
trim_blocks and lstrip_blocks
Sprinkling a dash on every tag gets tedious, so Jinja2 offers two settings that do it wholesale. trim_blocks removes the first newline after a block tag, and lstrip_blocks strips the leading whitespace before one. Set in the role's configuration, they give you clean output without decorating every tag with a -. For config templates this is the pragmatic default — turn both on and most loops and conditionals render tight automatically.
The trade-off is that these settings change the bytes of existing templates too. Flip them on a template that quietly relied on the old whitespace and its output shifts, sometimes in ways that matter. That is not a reason to avoid them — it is a reason to flip them deliberately, with the rendered diff in front of you, rather than as an afterthought that silently rewrites a dozen config files at once.
Undefined-Variable Behavior
By default, an undefined variable raises an error at render time. That strictness is exactly what you want for config files: a typo in a variable name fails the play loudly instead of rendering a blank into app.env and letting the app boot with a missing setting. The default is your friend, and the instinct to loosen it — so a missing variable "just renders empty" — is a bug waiting to ship a half-blank config.
The right way to handle a genuinely optional value is not to rely on undefined-becomes-empty but to guard it explicitly with default. {{ optional_setting | default('off') }} says out loud what the missing-value behavior is, where depending on silent emptiness leaves it implicit and fragile. Strict by default, explicit defaults where you mean them — that combination keeps a missing variable from ever silently reaching a live file.
Recursive Templating
Ansible re-renders a variable whose value itself contains {{ }}. That is usually a feature: a group_vars value like base_url: "https://{{ domain }}" resolves correctly because Ansible templates the value before using it. The problem is when a value contains literal braces it did not mean as template syntax — a regex like {{2,4}}, a Prometheus query, a password that happens to contain {{. Ansible tries to evaluate it as a template and either fails or mangles the value.
The fix is to mark the value as not-to-be-templated. Wrap it in {% raw %}...{% endraw %} in a template, or tag it !unsafe in a vars file, and Ansible leaves the braces alone. This is the last footgun in the chapter and the most surprising, because the data looks fine until the one run where a password with the wrong character in it brings down a deploy with an error about an undefined template variable that you never wrote.
- Writing a
{% for %}loop without trim markers ortrim_blocks, then shipping apg_hba.confor upstream block riddled with blank lines that makes every diff noisy and trips stricter parsers. - Putting
-%}on a line where you actually wanted the trailing newline, collapsing two config directives onto one line and breaking the file — trim markers cut both ways, and over-trimming is as broken as under-trimming. - Depending on an undefined variable rendering as empty instead of guarding it, so a typo'd variable name silently produces a blank value in
app.envand the app starts with a missing setting. - Storing a value that contains literal
{{ }}— a password, a regex like{{2,4}}, a Prometheus query — in a variable and watching Ansible try to template it; wrap it in{% raw %}/{% endraw %}or mark it!unsafe. - Setting
trim_blocks/lstrip_blocksglobally and being surprised a template that relied on the old whitespace now renders differently — whitespace settings change existing output, so flip them with the rendered diff in front of you.
- Turn on
trim_blocksandlstrip_blocksfor config templates so loops and conditionals produce clean output without a trim marker on every tag, and reserve explicit{%- -%}for the spots that need finer control. - Keep undefined-variable strictness on (the default) and guard genuinely optional values with
| default(...), so a missing variable fails loudly rather than rendering a silent blank into a live config. - Wrap any value containing literal
{{ }}— passwords, regexes, query languages — in{% raw %}/{% endraw %}or!unsafe, so Ansible does not try to re-template data that only looks like a template. - Diff the rendered output with
--check --diffagainst thetemplatetask when changing whitespace settings or trim markers, since these changes alter the file's bytes and you want to see exactly what moved. - Make the call about where a dash belongs by looking at the rendered file, not the template, so you trim the actual stray newline instead of guessing at it from the source.
{{- / -}} trim markers solve the identical problem in Helm and Consul-template
Terraform templatefile() shares the same Jinja-like whitespace concerns
ERB -%> trim mode, the Chef and Puppet equivalent
Knowledge Check
What do trim_blocks and lstrip_blocks each strip?
trim_blocksremoves the first newline after a block tag;lstrip_blocksstrips the leading whitespace before one- Both of them indiscriminately remove every blank line from the rendered output file, regardless of which tags produced them
trim_blockstrims the whitespace around{{ }}expressions, whilelstrip_blockstrims it around{% %}statement tags- They strip whitespace from the source template file itself, not the rendered output
Why is the default undefined-variable behavior (raise an error) desirable for config files?
- A typo'd variable name fails the play loudly instead of silently rendering a blank into a live config
- It makes templates render measurably faster by skipping over any unset variables
- It lets the managed node step in and fill in sensible defaults for any template values that were left missing on the control node
- It prevents the control node from reading optional variables into the template at all
A variable holds a password that contains literal {{ characters. What happens, and how do you stop it?
- Ansible tries to re-template the value and fails or mangles it; wrap it in
{% raw %}/{% endraw %}or mark it!unsafe - Ansible detects and escapes the embedded braces automatically before rendering, so nothing extra needs to be done about it
- The value renders perfectly fine on the control node, but the managed node then rejects it on receipt as a malformed config entry
- The braces are stripped out silently, leaving a truncated and unusable password
You are about to turn on trim_blocks for an existing role. What is the safe way to verify the change?
- Run the
templatetask with--check --diffto see exactly which bytes moved before applying - Trust the setting and apply it straight to production, since whitespace never affects valid configs
- Re-render the file on the managed node and compare the resulting hostnames
- Delete the live config first so there is a clean slate with nothing to diff against
You got correct