Jinja2 in Ansible¶
Ansible uses Jinja2 for both inline expressions in playbooks/tasks and for
ansible.builtin.template-rendered config files. The two contexts are different
but share the same engine - most of the pitfalls below apply to both.
Whitespace Control¶
By default, Jinja2 preserves newlines around block tags, which can produce blank lines in rendered output.
{%-strips whitespace before the tag.-%}strips whitespace after the tag.- You SHOULD use these consistently inside loops and conditionals to keep rendered files clean and diff-friendly.
You SHOULD set lstrip_blocks: true and trim_blocks: true in the template header to
apply stripping globally instead of per-tag:
Local Variables with {% set %}¶
Declare intermediate values to avoid repeating complex expressions:
{% set listen_addr = item.bind_address | default('0.0.0.0') %}
{% set listen_port = item.port | default(8080) %}
bind {{ listen_addr }}:{{ listen_port }};
Avoid deep-nesting filter chains inline - extract them into named variables for readability and easier review.
Macros for Repeated Config Blocks¶
You SHOULD use {% macro %} when the same structural block appears more than once or needs
to be parameterised:
{% macro server_block(name, port, upstream) %}
server {
server_name {{ name }};
listen {{ port }};
location / {
proxy_pass http://{{ upstream }};
}
}
{% endmacro %}
{% for vhost in vhosts %}
{{ server_block(vhost.name, vhost.port | default(80), vhost.upstream) }}
{% endfor %}
Macros keep templates DRY and make individual blocks independently testable.
Custom Filter Plugins for Complex Data¶
Macros emit text; they are not a data-manipulation tool. When a data
transformation is more than a short filter chain - it needs loops with
accumulators, builds a lookup index or joins lists, applies custom merge
semantics, or must validate input and fail with a clear message - implement
it as a custom filter plugin instead of assembling structures with {% set %}
and nested loops.
Keep a single builtin filter or a one-line selectattr / map chain inline. A
filter plugin adds a Python file that must be loaded, documented, and tested,
so only pay that cost when the template logic would cost more to maintain.
See developing-plugins.md for the full criteria and for writing and testing custom filter and test plugins.
Jinja2 Builtin Filters and Tests¶
Jinja2 expressions combine Jinja2 builtins with filter and test plugins from
ansible.builtin and from installed collections. Everything that is not in
the two lists below comes from a collection plugin and is called by FQCN
(see common.md).
The authoritative lists are in the Jinja2 Template Designer Documentation. Jinja2 3.1 provides the following builtins.
Jinja2 Builtin Filters¶
abs, attr, batch, capitalize, center, default (alias d),
dictsort, escape (alias e), filesizeformat, first, float,
forceescape, format, groupby, indent, int, items, join, last,
length (alias count), list, lower, map, max, min, pprint,
random, reject, rejectattr, replace, reverse, round, safe,
select, selectattr, slice, sort, string, striptags, sum,
title, tojson, trim, truncate, unique, upper, urlencode,
urlize, wordcount, wordwrap, xmlattr.
Jinja2 Builtin Tests¶
boolean, callable, defined, divisibleby, eq (aliases ==,
equalto), escaped, even, false, filter, float, ge (alias >=),
gt (aliases >, greaterthan), in, integer, iterable, le (alias
<=), lower, lt (aliases <, lessthan), mapping, ne (alias !=),
none, number, odd, sameas, sequence, string, test, true,
undefined, upper.
Filters and tests that look builtin but are not:
| Seems builtin | Provided by |
|---|---|
bool, to_json, to_nice_json, from_json |
ansible.builtin |
to_yaml, to_nice_yaml, from_yaml, from_yaml_all |
ansible.builtin |
combine, dict2items, items2dict, flatten |
ansible.builtin |
zip, split, regex_replace, ternary, b64encode |
ansible.builtin |
truthy, falsy, success, failed, changed |
ansible.builtin tests |
json_query |
community.general.json_query |
json_query lives in community.general. Bare json_query and
ansible.builtin.json_query also resolve because ansible.builtin carries a
legacy redirect to community.general.json_query, but the redirect only
works while that collection is installed and it hides where the plugin comes
from. Write community.general.json_query.
random and unique are the exception: they exist in both namespaces and
ansible.builtin wins for the bare name. Bare random accepts a number or
seed= like ansible.builtin.random, not Jinja2's sequence-only version. Use
the FQCN when the Ansible behaviour matters.
Jinja2's JSON filter is tojson and emits an HTML-safe string;
ansible.builtin.to_json emits plain JSON. They are not interchangeable.
Since ansible-core 2.19 each Jinja2 builtin is also addressable as
ansible.builtin.<name> (ansible.builtin.abs); earlier cores accept only
the bare name. Keep Jinja2 builtins bare so expressions stay portable across
ansible-core versions.
Generating Structured Config Files with JSON / YAML Filters¶
When a config file format is JSON or YAML, avoid constructing it with raw string interpolation. Instead, build the data structure in Ansible variables and serialise it with a filter:
- name: Write application config
ansible.builtin.copy:
content: "{{ app_config | ansible.builtin.to_nice_yaml(indent=2) }}"
dest: /etc/myapp/config.yaml
mode: "0644"
| Filter | Output |
|---|---|
\| ansible.builtin.to_json |
Compact JSON |
\| ansible.builtin.to_nice_json(indent=2) |
Pretty-printed JSON |
\| ansible.builtin.to_yaml |
Compact YAML |
\| ansible.builtin.to_nice_yaml(indent=2) |
Human-readable YAML |
You SHOULD prefer this pattern over ansible.builtin.template for config files whose
entire content is a single data structure. It keeps the source of truth in
variables (where it can be validated, merged with ansible.builtin.combine, and overridden) rather than
scattered across a template.
Use ansible.builtin.template when the file mixes structured data with prose,
comments that must be preserved, or requires per-line conditional logic.
List and Dict Operations¶
Use Jinja2 filters instead of tasks to transform data inline:
{# Filter a list to only enabled items #}
{% set active = items | selectattr('enabled', 'true') | list %}
{# Extract a single attribute from each item #}
{% set names = items | map(attribute='name') | list %}
{# Merge two dicts (right side wins on conflict) #}
{% set merged = defaults | ansible.builtin.combine(overrides) %}
{# Flatten a nested list #}
{% set flat = nested_list | ansible.builtin.flatten %}
Default Values and Falsy Variables¶
The or operator and the | default() filter interact with falsy values in
different ways. Choosing the wrong one is a common source of silent bugs.
The or Operator Pitfall¶
Consider a variable that holds a valid value that may legitimately be falsy
(e.g., an integer index set to 0, a boolean flag set to false, an empty
list that means "no items"). Using or as a fallback:
When primary_value is 0, Jinja2 evaluates 0 or fallback_value and returns
fallback_value - silently ignoring the explicitly configured value.
Any falsy value (0, false, "", [], {}) causes or to skip to the
right-hand side, regardless of whether the left side was intentionally set.
You SHOULD NOT use var_a or var_b as a fallback pattern whenever the primary variable can
legitimately hold a falsy value.
| default(fallback) - undefined only¶
Substitutes fallback only when the variable is undefined (raises a Jinja2
UndefinedError). If primary_value is defined as 0 or false, this
returns the defined value correctly.
You SHOULD use this when the variable may not be set at all, but any defined value -
including falsy ones like 0, false, "" - should be preserved as-is.
| default(fallback, true) - undefined or falsy¶
The second argument true (the boolean parameter) extends the substitution
condition: fallback is used when the variable is undefined OR falsy.
0, false, "", [], {}, and None all trigger the fallback.
This behaves identically to or for falsy values:
0 | default(1, true) -> 1.
You SHOULD use this only when a falsy value genuinely means "not provided" - for example, falling back to a non-empty string when a variable is an empty string, or to a non-zero value when zero carries no meaningful value in context.
Choosing Between Them¶
| Scenario | Correct pattern |
|---|---|
Variable may be undefined; any defined value (incl. 0, false) is valid |
\| default(fallback) |
| Variable may be undefined or empty string; non-empty string default needed | \| default(fallback, true) |
Variable may be undefined or false; non-false default needed |
\| default(fallback, true) |
Variable may be undefined or 0; non-zero default and 0 is not meaningful here |
\| default(fallback, true) |
| Variable is always defined; protect against accidental falsy logic | Use when: var is defined separately |
Undefined Variable Behaviour¶
Ansible uses Jinja2's StrictUndefined mode by default - referencing an
undefined variable raises an error immediately rather than silently producing an
empty string.
- Use
| default(...)to provide a fallback for variables that may not exist. - Use
is defined/is undefinedinwhen:conditions to guard task execution:
- You MUST NOT rely on undefined variables silently collapsing to
""- this behaviour is not guaranteed and hides bugs.