Lazy templating and global variables in Ansible 2.19+

Hi all,

We are in the process of upgrading from Ansible 2.18 to 2.20 and are aware that there were some significant changes to templating in 2.19.

What would be the best practice for handling “undefined” variables in templates in Ansible 2.19+? For example, we have a bunch of global variables defined in our group_vars/all.yml, one of them is:

restic_repository: "s3:s3.amazonaws.com/{{ backup.aws_bucket_name }}"

backup dictionaries are defined in each of the inventories’ group_vars/all.yml and restic_repository was being lazily evaluated when needed in Ansible 2.18.

However, we have a play that references another global variable earlier in the run, so I guess the whole global group_vars/all.yml is loaded and evaluated in 2.19, and we get an error in that play:

FAILED! => {"changed": false, "msg": "Task failed: 'backup' is undefined"}

What would be considered best practice here? use default() in the template? Define a dummy backup dictionary in the global group_var/all.yml?

Thank you!

Variable templating is lazier in 2.19 than 2.18, and a task that uses one variable in group_vars/all.yml does not result in templating unrelated variables in group_vars/all.yml. Can you provide more context, like the full error (which shows the origin of the undefined variable) and failing task + surrounding context that previously succeeded in 2.18?

The failing task is the second stat below:

- name: "[{{ elastic_prefix }}] Set create certificates flag"
  run_once:  true
  block:
    - name: "[{{ elastic_prefix }}] Stat CA cert file"
      stat:
        path: "{{ elastic_host_certificates_path }}/{{ elastic_ca_cert_filename }}"
      register: elastic_ca_cert_stat
    - name: "[{{ elastic_prefix }}] Stat node cert files"
      stat:
        path: "{{ elastic_host_certificates_path }}/{{ elastic_prefix }}-{{ elastic_node_host.inventory_hostname_short }}.crt"
      loop: "{{ groups['docker-swarm'] | map('extract', hostvars) | list }}"
      loop_control:
        loop_var: elastic_node_host
      register: elastic_node_certs_stat
 #  more tasks ...

elastic_prefix and elastic_host_certificates_path are local variables, but elastic_host_certificates_path references a global variable set in global_vars/all.yml.

The full error text is:

[ERROR]: Task failed: 'backup' is undefined

Task failed.
Origin: <path>/roles/elasticsearch/tasks/elastic_instance.yml:50:7

48         path: "{{ elastic_host_certificates_path }}/{{ elastic_ca_cert_filename }}"
49       register: elastic_ca_cert_stat
50     - name: "[{{ elastic_prefix }}] Stat node cert files"
         ^ column 7

<<< caused by >>>

'backup' is undefined
Origin:  <path>/group_vars/all.yml:128:31

126   --prune
127 # Restic backup bucket name is set in cluster's all.yml file.
128 restic_repository:            "s3:s3.amazonaws.com/{{ backup.aws_bucket_name }}"
                                  ^ column 31

elastic_instance.yml is included (include_tasks) from role’s main.yml.

restic_repository variable is referenced in a later play and different role, it’s never used in the elasticsearch role.

This was run on Debian 13.3 WSL on Windows 10, in a Python 3.13.5 venv with the following pip packages installed:

Package            Version
------------------ -----------
ansible            13.4.0
ansible-core       2.20.3
argcomplete        3.6.3
bcrypt             5.0.0
boto               2.49.0
boto3              1.42.60
botocore           1.42.60
certifi            2026.2.25
cffi               2.0.0
charset-normalizer 3.4.4
cryptography       46.0.5
dnspython          2.8.0
hvac               2.4.0
idna               3.11
Jinja2             3.1.6
jmespath           1.1.0
MarkupSafe         3.0.3
packaging          26.0
pip                26.0.1
pycparser          3.0
python-dateutil    2.9.0.post0
PyYAML             6.0.3
requests           2.32.5
resolvelib         1.2.1
s3transfer         0.16.0
six                1.17.0
tomlkit            0.14.0
urllib3            2.6.3
xmltodict          1.0.4
yq                 3.4.3

Was also replicated on a MacOS, a Python 3.14 venv and the same set of packages. If we downgrade to Ansible 2.18.14, everything works.

1 Like

Could you share more of group_vars/all.yml prior to the definition of restic_repository? It’s acting like that variable definition is being sucked up into something that comes before that.

If the backup definition from relevant group_vars wasn’t being pulled in (because of inventory differences?), then we still wouldn’t expect the error to be thrown in the elastic_instance.yml task file since it’s not used until later.

Does

grep -r restic_repository .

turn up any unexpected usages?

'T’is a puzzler.

I think the usage is shown, the cause is

      loop: "{{ groups['docker-swarm'] | map('extract', hostvars) | list }}"

I was able to reproduce, but need to do some more research. In earlier versions of ansible-core, undefined variables in hostvars were just the template string, which was a bug.

2 Likes

Oooh, nice catch! I can test tomorrow and use hostvars in a debug task, with and without a loop and map.