Ansible lookups are really useful for injecting dynamic data into your plays. You can use lookups for situations where you want to add a timestamp comment to a file, record the user running the playbook, generate random passwords, or fetch config values from etcd.

Recently, I discovered something unexpected about how lookups in variable definitions. When I set a variable at the beginning of a play, I would expect it's value to remain the same throughout the play. After all, it is a variable, not a function.

In the following example, I'm storing the current time in a variable at the beginning of my play.

- name: lookups in variables vs. lookups in facts
  hosts: localhost
  vars:
    var_time: "Var: {{lookup('pipe', 'date \"+%H:%M:%S\"')}}"

Now I print the value of the variable, wait a couple seconds, and print it again.

tasks:
    - debug: var=var_time
    - command: sleep 2
    - debug: var=var_time

I would expect it to be the same value both times, but if you look at the output, you can see that the value changes during the course of the play.

TASK: [debug var=var_time] ****************************************************
ok: [127.0.0.1] => {
    "var": {
        "var_time": "Var: 09:30:40"
    }
}

TASK: [command sleep 2] *******************************************************
changed: [127.0.0.1]

TASK: [debug var=var_time] ****************************************************
ok: [127.0.0.1] => {
    "var": {
        "var_time": "Var: 09:30:42"
    }
}

The original value was "Var: 09:30:40", but after a 2 second sleep, it is now "Var: 09:30:42". It appears that rather than storing the variable at the start of the play and using the lookup value throughout, ansible (or possibly jinja2) is inlining the variable. If we do the inlining ourselves, it makes more sense why the value changes.

- debug: { msg: "Var: {{lookup('pipe', 'date \"+%H:%M:%S\"')}}'" }
   - command: sleep 2
   - debug: { msg: "Var: {{lookup('pipe', 'date \"+%H:%M:%S\"')}}" }

While sometimes this may be useful, other times it is a problem. In my case, I wanted to use a timestamp as part of an identifier that I needed to reference in later tasks. If the value of my variable is always changing, I can't do that.

Instead, I found that the set_fact module only evaluates the lookup when you first set the fact. Then you can use that fact, in later tasks and its value will always be the same. with lookups to

tasks:
    - set_fact:
        fact_time: "Fact: {{lookup('pipe', 'date \"+%H:%M:%S\"')}}"

    - debug: var=fact_time
    - command: sleep 2
    - debug: var=fact_time

And in the output, we see that the fact_time value remains the same before and after the sleep.

TASK: [set_fact ] *************************************************************
ok: [127.0.0.1]

TASK: [debug var=fact_time] ***************************************************
ok: [127.0.0.1] => {
    "var": {
        "fact_time": "Fact: 09:30:42"
    }
}

TASK: [command sleep 2] *******************************************************
changed: [127.0.0.1]

TASK: [debug var=fact_time] ***************************************************
ok: [127.0.0.1] => {
    "var": {
        "fact_time": "Fact: 09:30:42"
    }
}

That's better.

We've discovered a subtle difference between how ansible variables and facts work. Ansible evaluates lookups for a variable every time that variable is used in the task at the time the task is run. That means the lookup value could be different in different tasks. If you want to get a lookup value once and use the same value for every task in a play, you need to register a fact with the set_fact module instead.

Hopefully, I've made the difference clear and you won't get stuck on this for as long as I was.