Templating errors used to be the worst diagnostic experience in Ansible.
A Python traceback would surface, name a file that was not the file with
the mistake in it, and leave you grepping for braces.
On ansible-core 2.21 that changed, and the change is worth learning
deliberately because the new output answers a question the old one did
not: where did the offending value come from?
The caused-by chain
Every templating failure on 2.21.3 is reported as a chain of frames,
outermost first, each with an Origin: and a source excerpt.
Read-only / Safean undefined variable, in full — executed on ansible-core 2.21.3— A single debug task referencing a variable nobody defined. Local connection, nothing modified. The output is shown complete because its structure is the lesson.
$ ansible-playbook -i inv.ini j1.yml
[ERROR]: Task failed: Finalization of task args for 'ansible.builtin.debug'
failed: Error while resolving value for 'msg': 'app_port' is undefined
Origin: /home/ops/estate/j1.yml:4:7
2 gather_facts: false
3 tasks:
4 - name: Undefined variable
^ column 7
<<< caused by >>>
Finalization of task args for 'ansible.builtin.debug' failed.
Origin: /home/ops/estate/j1.yml:5:7
3 tasks:
4 - name: Undefined variable
5 ansible.builtin.debug:
^ column 7
<<< caused by >>>
Error while resolving value for 'msg': 'app_port' is undefined
Origin: /home/ops/estate/j1.yml:6:14
4 - name: Undefined variable
5 ansible.builtin.debug:
6 msg: 'listening on {{ app_port }}'
Read it bottom-up. The last frame is the innermost cause and the one
with the precise location: file, line 6, column 14, with the source line
printed. The frames above it are the layers that failed as a
consequence: the argument could not be finalised, so the task failed.
The five shapes
Undefined variable
Error while resolving value for 'msg': 'app_port' is undefined
The name is exactly the name that failed to resolve — including, in a
chain like item.shell, the part that was missing. It means the
variable does not exist at this point in this play for this host,
which is not the same as “does not exist in the repository”. The
previous lesson is the procedure.
The fix is either to define it or to supply a default, and choosing
between those is a design decision: a default hides a missing value, and
for something like a port or a hostname you usually want the failure.
Syntax error in the template
Syntax error in template: unexpected 'end of print statement'
Read-only / Safea malformed expression, innermost frame only — executed on 2.21.3— The last frame of the chain for msg: {{ 1 + }}. Column 14 is the opening of the expression.
$ ansible-playbook -i inv.ini j5.yml
Error while resolving value for 'msg': Syntax error in template: unexpected
'end of print statement'
Origin: /home/ops/estate/j5.yml:16:14
14 - name: unexpected token
15 ansible.builtin.debug:
16 msg: "{{ 1 + }}"
^ column 14
The message names what the parser expected next. unexpected 'end of print statement' means the expression ended early — a missing operand,
an unclosed bracket, a filter with no argument. unexpected '}' usually
means a nested {{ }} inside an expression that was already an
expression.
Filter applied to the wrong type
The filter plugin 'ansible.builtin.join' failed: 'int' object is not iterable
Read-only / Safea filter chain where one step changed the type — executed on 2.21.3— app_name is a string; length makes it an int; join then has nothing to iterate. Innermost frame shown.
$ ansible-playbook -i inv.ini j2.yml
Error while resolving value for 'msg': The filter plugin 'ansible.builtin.join'
failed: 'int' object is not iterable
Origin: /home/ops/estate/j2.yml:8:14
6 - name: Filter applied to the wrong type
7 ansible.builtin.debug:
8 msg: '{{ app_name | length | join(",") }}'
The named filter is the one that failed, not necessarily the one that
is wrong. Here join failed because length before it produced an
integer. In a long filter chain, work leftwards from the named filter
and print the intermediate values with debug and var: until the
type changes to something you did not expect.
A conditional that is not a boolean
This is the shape that changed most on 2.21, and it deserves its own
section.
Read-only / Safewhen: enable_tls where enable_tls is the string 'false' — executed on 2.21.3— The variable is defined as a quoted string. On 2.21.3 the conditional refuses to evaluate it, and the innermost frame points at the definition rather than at the when clause.
$ ansible-playbook -i inv.ini j3.yml
A 'when' expression failed.
Origin: /home/ops/estate/j3.yml:9:13
7 ansible.builtin.debug:
8 msg: 'TLS branch taken'
9 when: enable_tls
^ column 13
<<< caused by >>>
Conditional result (True) was derived from value of type 'str' at
'/home/ops/estate/j3.yml:4:17'. Conditionals must have a boolean result.
Origin: /home/ops/estate/j3.yml:9:13
Templating delimiters in when
when takes a bare Jinja expression. Writing when: "{{ enable_tls }}"
wraps it in an extra evaluation.
Verified on 2.21.3: with enable_tls set to the string 'false', the
braces form produced skipping with no error and no warning, where the
bare form failed loudly. The two forms are not equivalent, and the
braces form is the one that can quietly do the wrong thing.
Write conditionals bare:
Read-only / Safeconditionals, correct and incorrect— Structure only; nothing is executed. The first form is correct, the second is the one to remove during review.
# correct — when takes a bare expression
- name: Configure TLS
ansible.builtin.template:
src: tls.conf.j2
dest: /etc/app/tls.conf
mode: '0640'
when: enable_tls | bool
# incorrect — the extra evaluation changes the semantics
- name: Configure TLS
ansible.builtin.template:
src: tls.conf.j2
dest: /etc/app/tls.conf
mode: '0640'
when: "{{ enable_tls }}"
default() versus default(omit)
default(value) substitutes a value. default(omit) substitutes a
special marker that tells Ansible to remove the argument entirely,
so the module applies its own default.
Read-only / Safeomit, used where it is meaningful— Where shell is absent from an item, the shell argument is not passed to the module at all, so the system default applies. Structure only; the user module is shown for illustration and is not executed here.
- name: Create accounts, letting the system choose the shell where unspecified
ansible.builtin.user:
name: '{{ item.name }}'
shell: '{{ item.shell | default(omit) }}'
state: present
loop: '{{ users }}'
Knowledge check
Knowledge check · 4 questions
Q1. A templating error on ansible-core 2.21.3 prints three frames separated by caused-by markers. Which frame has the precise location of the mistake?
Q2. On ansible-core 2.21.3, a play sets enable_tls: false as a quoted string and a task has when: enable_tls. What is true? Select all that apply.
Q3. when: "{{ enable_tls }}" and when: enable_tls are equivalent, since Jinja evaluates the expression either way.
Q4. An expression reads {{ app_name | length | join(",") }} and fails with the join filter reporting that an int object is not iterable. Where is the mistake?
Passing score: 75%. Answers are checked in this browser.