More ways to set variables for a scope

Right now, there are two ways to set variables in Ansible inside a role/playbook:

  1. Use the vars keyword. This creates a lazyily evaluated variable that’s evaluated every time it is used.
  2. Use the ansible.builtin.set_fact action. This creates a host-level variable with a fixed value that is computed once. (With run_once: true the same value can be used for all hosts.) The value is set until the play ends, or it is overridden by a subsequent ansible.builtin.set_fact action.

(There’s also the ansible.builtin.include_vars plugin, which allows to load lazily evaluated variables from a YAML or JSON file, which are then defined until the end of the play. And then roles can define variables that still exist after the role finished, depending on the setting of DEFAULT_PRIVATE_ROLE_VARS.)

It would be great to have a mix between vars and ansible.bulitin.set_fact, which allows define a host-level variable whose value is computed once, and which is only valid for a specific scope. This is very helpful if you for example want to set a variable to a secret token that is loaded from some secret store, and you don’t want one API request to the secret store for every single use of that variable. (And you don’t want the token to be set anymore once the scope exits.)

Right now you have to use ansible.builtin.set_fact twice together with a block and always (set the variable to an empty string in the always, for example, to “clear” it), which is both ugly and leaks the variable name to later tasks.

Another thing that would be great would be to have a way to include variables from a JSON/YAML file (or a SOPS encrypted file, so it would be great if collection plugins could do this as well) only during a scope. So basically ansible.builtin.include_vars, but the variables stop being around at the end of the scope (likely a block).

I don’t have a good suggestion how this should look - I mainly want to trigger a discussion, which hopefully spawns a proposal. So: what do you think? (I’ve been thinking about this more than once, and got reminded while writing in How to retrieve fact with a dynamic fact name? .)

4 Likes

Off the top of my head:

  • I can imagine some sort of introspection mechanism to determine the precedence of a variable. (Perhaps an option of defined? I guess that would make it a filter rather than a test though. Hm.)

  • What about a variation of set_fact that – in addition to selecting from a subset of scope/precedence –allowed choosing whether the variable created was static or lazily evaluated.

  • It seems like anything that can be set with set_fact should also be un-set_fact-ible. Including persistent cacheable facts. Perhaps it should even be possible to “undefine” other variables, or mask them so they look undefined within specific scopes.

Flip side: Let’s not implement anything merely because we can. There should be a plausible use case for such changes. Let’s not make the implementers/maintainers’ jobs harder because it seemed like a cool idea in some thread way back in 2025. But I think at one time or another I’ve wanted each of these features. Obviously I’ve found ways to live without. There is a balance to be struck.

2 Likes

Trying to think of requirements here, I read two different types of scopes we might want to address (first shot at names, gladly taking better suggestions):

  • target scope: whether it would be all, groups or specific hosts
  • code/locality scope: playbook, play, task, role, file, maybe other locations

Anyways, this spells changes in ansible-core, which might be hard to achieve and maintain backward compatibility. Not holding my breath.

Ansible has 3 scopes:

  • extra vars: global and overrides both other scopes
  • play object: attached to the play object and inheritable and overridable by contained play objects
  • host: attached to the host for the duration of the run

I attempted a simpler implementation of something close to what you want:

vars:
    x: !static {{ ... }}

Which would compute the variable the first time it was encountered and cache the result, but this has many issues, like it could yield very different and undesired results depending on if you wanted this per host or per play object and where the scope variables are sourced from, which the templating engine did not know or care.

The 2.19 templating revamp can help with this a bit, but I don’t believe it will resolve all the issues.
This probably requires a redesign of how variables work in Ansible, see redesign variable interface · Issue #127 · ansible/proposals · GitHub for one such fever dream.

1 Like

I don’t have any direct answers, other than to say we want to basically rewrite VariableManager to utilize a ChainMap or some derivative of it, and I think until we can get around to re-architecting this functionality, I don’t see us making any changes to variable scoping and precedence.

One place something like this would help is if we had a mechanism to support the coercing functionality for play/role argspecs. Right now the argspec validator can only assert that it could be coerced to the right type, but cannot actually coerce it to that type for future use. It’s undecided if we will go down this path, but if we did, we’d need a mechanism to replace the variable at it’s current level, so as to not break precedence elsewhere, and we don’t have anything to permit that currently.

1 Like