From eda5de71f1cb383170dd2e7fc3a44435d2d13439 Mon Sep 17 00:00:00 2001 From: Mike Schwager Date: Mon, 4 Aug 2025 12:53:02 -0500 Subject: [PATCH 1/2] Include section on lazy vs eager variable evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a new section to the Variables guide titled “Lazy vs. Eager Variable Evaluation”. It expands on the existing documentation around variable precedence by demonstrating the difference between eager and lazy evaluation of variables — specifically comparing set_fact (eager) with vars: (lazy). It includes a self-contained example that walks through: How variables set with set_fact are evaluated and fixed at the time they are run, how variables defined in vars: blocks are lazily re-evaluated each time they're referenced, and the effect of --extra-vars on both eager and lazy variables. Also includes an illustration of variable shadowing — showing that vars: blocks can override some variables but not those set using set_fact (due to variable precedence rules). The section is placed immediately after Variable Precedence: Where Should I Put a Variable? and before Using Advanced Variable Syntax to provide a natural learning progression from where and how variables are resolved, to when they're evaluated. All code and output are copy/paste testable, and align with live Ansible behavior as Ansible core 2.14.2. This update aims to clarify a subtle aspect of Ansible variable resolution. --- .../playbook_guide/playbooks_variables.rst | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/docs/docsite/rst/playbook_guide/playbooks_variables.rst b/docs/docsite/rst/playbook_guide/playbooks_variables.rst index 60bc735bdd..0b48f7fa2a 100644 --- a/docs/docsite/rst/playbook_guide/playbooks_variables.rst +++ b/docs/docsite/rst/playbook_guide/playbooks_variables.rst @@ -551,6 +551,186 @@ There are some protections in place to avoid the need to namespace variables. In Instead of worrying about variable precedence, we encourage you to think about how easily or how often you want to override a variable when deciding where to set it. If you are not sure what other variables are defined and you need a particular value, use ``--extra-vars`` (``-e``) to override all other variables. +.. _lazy_vs_eager_evaluation: + +Lazy vs. Eager Variable Evaluation +================================== + +Ansible variable resolution not only depends on **precedence**, but also on **when** a variable is **evaluated**. Some variable declarations (like ``set_fact``) are evaluated immediately (eager), while others (like those defined in ``vars:`` blocks) are **template expressions** and are evaluated **only when used** (lazy). + +This distinction matters when the variable a template depends on changes later in the play. For example, if you copy the value of a variable using ``set_fact``, it remains frozen. But if you reference that same variable in a ``vars:`` block with a Jinja2 expression, it can pick up changes made later in the playbook. + +The following example demonstrates this (assume this playbook is installed in a file named ``lazy_eager.yml``). Additionally, it demonstrates when shadowing a variable works, and when it doesn't: + +.. code-block:: yaml + + - hosts: localhost + gather_facts: false + vars: + message: "ORIGINAL" + + tasks: + - name: Capture message value in eager_var using set_fact + set_fact: + eager_var: "{{ message }}" + + - name: Define lazy_var using vars block (will re-evaluate) + vars: + lazy_var: "{{ message }}" + block: + - name: Step 1 — Show all vars before message change + debug: + msg: + - "eager_var (captured earlier): {{ eager_var }}" + - "lazy_var (current message): {{ lazy_var }}" + - "message: {{ message }}" + + - name: Change message to 'CHANGED' + set_fact: + message: "CHANGED" + + - name: Step 2 — Show all vars after message change + debug: + msg: + - "eager_var (should still be ORIGINAL): {{ eager_var }}" + - "lazy_var (should now be CHANGED): {{ lazy_var }}" + - "message: {{ message }}" + + - name: Step 3 - Locally shadow the variables + vars: + eager_var: "local eager" + lazy_var: "local lazy" + message: "local message" + debug: + msg: + - "local eager (shadowed?): {{ eager_var }}" + - "local lazy_var (shadowed?): {{ lazy_var }}" + - "local message (shadowed?): {{ message }}" + + - name: Step 4 — Show all vars after message change, and shadowing + debug: + msg: + - "eager_var (should still be ORIGINAL): {{ eager_var }}" + - "lazy_var (should now be CHANGED): {{ lazy_var }}" + - "message: {{ message }}" + +.. rubric:: Example Run (without extra-vars) + +.. code-block:: text + + $ ansible-playbook -i localhost, lazy_eager.yml + + PLAY [localhost] ******************************************************************************************************************************************** + + TASK [Capture message value in eager_var using set_fact] **************************************************************************************************** + ok: [localhost] + + TASK [Step 1 — Show all vars before message change] ********************************************************************************************************* + ok: [localhost] => { + "msg": [ + "eager_var (captured earlier): ORIGINAL", + "lazy_var (current message): ORIGINAL", + "message: ORIGINAL" + ] + } + + TASK [Change message to 'CHANGED'] ************************************************************************************************************************** + ok: [localhost] + + TASK [Step 2 — Show all vars after message change] ********************************************************************************************************** + ok: [localhost] => { + "msg": [ + "eager_var (should still be ORIGINAL): ORIGINAL", + "lazy_var (should now be CHANGED): CHANGED", + "message: CHANGED" + ] + } + + TASK [Step 3 - Locally shadow the variables] **************************************************************************************************************** + ok: [localhost] => { + "msg": [ + "local eager (shadowed?): ORIGINAL", + "local lazy_var (shadowed?): local lazy", + "local message (shadowed?): CHANGED" + ] + } + + TASK [Step 4 — Show all vars after message change, and shadowing] ******************************************************************************************* + ok: [localhost] => { + "msg": [ + "eager_var (should still be ORIGINAL): ORIGINAL", + "lazy_var (should now be CHANGED): CHANGED", + "message: CHANGED" + ] + } + + PLAY RECAP ************************************************************************************************************************************************** + localhost : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +As shown above: + +- ``eager_var`` was evaluated once using ``set_fact`` and remains unchanged. +- ``lazy_var`` was defined with ``vars:`` and holds a Jinja2 expression. Its value is re-evaluated each time it's used, so it picks up the new value of ``message``. +- Variables set using set_fact cannot be shadowed by vars: blocks because facts are stored at the host level and have higher precedence than task-level variables. + +This example illustrates the subtle but important distinction between storing a value (eager evaluation) and referencing an expression (lazy evaluation). + +.. note:: + ``--extra-vars`` will override both ``set_fact`` and ``vars:`` unless explicitly shadowed. See :ref:`variable precedence ` for more details. If you set the ``message`` variable using, for example, ``--extra-vars="message='CLI'"``, then all outputs will be as follows: + +.. rubric:: Example Run (with extra-vars) + +.. code-block:: text + + $ ansible-playbook -i localhost, lazy_eager.yml --extra-vars="message='CLI'" + + PLAY [localhost] ******************************************************************************************************************************************** + + TASK [Capture message value in eager_var using set_fact] **************************************************************************************************** + ok: [localhost] + + TASK [Step 1 — Show all vars before message change] ********************************************************************************************************* + ok: [localhost] => { + "msg": [ + "eager_var (captured earlier): CLI", + "lazy_var (current message): CLI", + "message: CLI" + ] + } + + TASK [Change message to 'CHANGED'] ************************************************************************************************************************** + ok: [localhost] + + TASK [Step 2 — Show all vars after message change] ********************************************************************************************************** + ok: [localhost] => { + "msg": [ + "eager_var (should still be ORIGINAL): CLI", + "lazy_var (should now be CHANGED): CLI", + "message: CLI" + ] + } + + TASK [Step 3 - Locally shadow the variables] **************************************************************************************************************** + ok: [localhost] => { + "msg": [ + "local eager (shadowed?): CLI", + "local lazy_var (shadowed?): local lazy", + "local message (shadowed?): CLI" + ] + } + + TASK [Step 4 — Show all vars after message change, and shadowing] ******************************************************************************************* + ok: [localhost] => { + "msg": [ + "eager_var (should still be ORIGINAL): CLI", + "lazy_var (should now be CHANGED): CLI", + "message: CLI" + ] + } + + PLAY RECAP ************************************************************************************************************************************************** + localhost : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + Using advanced variable syntax ============================== From 0815b41aba24ba2df4f00f214186155da41ceabc Mon Sep 17 00:00:00 2001 From: Mike Schwager Date: Mon, 4 Aug 2025 14:36:46 -0500 Subject: [PATCH 2/2] Update playbooks_variables.rst Corrected reference to ansible_variable_precedence --- docs/docsite/rst/playbook_guide/playbooks_variables.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docsite/rst/playbook_guide/playbooks_variables.rst b/docs/docsite/rst/playbook_guide/playbooks_variables.rst index 0b48f7fa2a..c6dc5d54bf 100644 --- a/docs/docsite/rst/playbook_guide/playbooks_variables.rst +++ b/docs/docsite/rst/playbook_guide/playbooks_variables.rst @@ -676,7 +676,7 @@ As shown above: This example illustrates the subtle but important distinction between storing a value (eager evaluation) and referencing an expression (lazy evaluation). .. note:: - ``--extra-vars`` will override both ``set_fact`` and ``vars:`` unless explicitly shadowed. See :ref:`variable precedence ` for more details. If you set the ``message`` variable using, for example, ``--extra-vars="message='CLI'"``, then all outputs will be as follows: + ``--extra-vars`` will override both ``set_fact`` and ``vars:`` unless explicitly shadowed. See :ref:`variable precedence ` for more details. If you set the ``message`` variable using, for example, ``--extra-vars="message='CLI'"``, then all outputs will be as follows: .. rubric:: Example Run (with extra-vars)