Ansible variable has inconsistent values

Viewed 266

I've been playing with ansible for the last couple of days and have run into a behavior that makes no sense to me. I can reproduce this with a simple playbook that runs two roles. Each role sets a variable, then imports a chain of other roles. The other roles do nothing but print out the value of the variable, then import the next role in the chain. The output I get is:

$ ansible-playbook site.yml

"msg": "role_1a v1 = role_1a default"
"msg": "role_2 v1 = role_1a default"
"msg": "role_3 v1 = role_1a default"
"msg": "role_4 v1 = role_1b default" <=== expected role_1a default
"msg": "role_5 v1 = role_1b default" <=== expected role_1a default

"msg": "role_1b v1 = role_1b default"
"msg": "role_2 v1 = role_1b default"
"msg": "role_3 v1 = role_1a default" <=== expected role_1b default
"msg": "role_4 v1 = role_1b default"
"msg": "role_5 v1 = role_1b default"

I've read the section on variables about twenty times now and the only thing I can see that may have a bearing on this is "Since Ansible 2.7 variables defined in vars and defaults for the role are exposed at playbook parsing time", which I suppose could mean that variable inheritance at execution time is a crapshoot.

# site.yml
- hosts: testserver
  roles:
    - role_1a
    - role_1b

# roles/role_1a/defaults/main.yml
v1: "role_1a default"

# roles/role_1a/tasks/main.yml
- debug: msg="role_1a v1 = {{ v1 }}"
- import_role: name=role_2

# roles/role_2/tasks/main.yml
- debug: msg="role_2 v1 = {{ v1 }}"
- import_role: name=role_3

# roles/role_3/tasks/main.yml
- debug: msg="role_3 v1 = {{ v1 }}"
- import_role: name=role_4

# roles/role_4/tasks/main.yml
- debug: msg="role_4 v1 = {{ v1 }}"
- import_role: name=role_5

# roles/role_5/tasks/main.yml
- debug: msg="role_5 v1 = {{ v1 }}"

# roles/role_1b/defaults/main.yml
v1: "role_1b default"

# roles/role_1b/tasks/main.yml
- debug: msg="role_1b v1 = {{ v1 }}"
- import_role: name=role_2

# hosts
[testing]
testserver

# /etc/ansible/ansible.cfg
[defaults]
[inventory]
[privilege_escalation]
[paramiko_connection]
[ssh_connection]
[persistent_connection]
[accelerate]
[selinux]
[colors]
[diff]

$ ansible --version
ansible 2.9.3
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/joe/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
re
1 Answers

Q: "The fact that apparently everyone else seems to be able to use variables without issues, I am more inclined to believe that there is some fundamental concept I missed along the way."

A: Not the case, unfortunately. Very probably it's an Ansible issue. There are a dozen include and import issues. Some of them are reminding the problem described here


I was able to reproduce the reported result. The play

- hosts: localhost
  roles:
    - role_1a
    - role_1b

gives

    "msg": "role_1a v1 = role_1a default"
    "msg": "role_2 v1 = role_1a default"
    "msg": "role_3 v1 = role_1a default"
    "msg": "role_4 v1 = role_1b default"
    "msg": "role_5 v1 = role_1b default"
    "msg": "role_1b v1 = role_1b default"
    "msg": "role_2 v1 = role_1b default"
    "msg": "role_3 v1 = role_1a default"
    "msg": "role_4 v1 = role_1b default"
    "msg": "role_5 v1 = role_1b default"

Let's make sure the order of the messages is correct. Changed tasks (similar role_1a,2,3,4)

# roles/role_1b/tasks/main.yml
- debug: msg="role_1b v1 = {{ v1 }}"
- import_role:
    name: role_2
  vars:
    root: "role_1b"

# roles/role_5/tasks/main.yml
- debug: msg="root = {{ root }} | role_5 v1 = {{ v1 }}"

give

    "msg": "role_1a v1 = role_1a default"
    "msg": "root = role_1a | role_2 v1 = role_1a default"
    "msg": "root = role_1a | role_3 v1 = role_1a default"
    "msg": "root = role_1a | role_4 v1 = role_1b default"
    "msg": "root = role_1a | role_5 v1 = role_1b default"
    "msg": "role_1b v1 = role_1b default"
    "msg": "root = role_1b | role_2 v1 = role_1b default"
    "msg": "root = role_1b | role_3 v1 = role_1b default" <=== was: role_1a default
    "msg": "root = role_1b | role_4 v1 = role_1b default"
    "msg": "root = role_1b | role_5 v1 = role_1b default"

It shows the order of the messages is correct. But, the result changed. Now the result of role_1b is as expected.


Q: "Namespace the variables to avoid conflicts. What is namespacesing?"

A: "Namespacing" means to "define the variable", or in particular "register the variable with the host", i.e. put the variable into the hostvars. For example

# roles/role_1a/tasks/main.yml
- set_fact:
    v1: "{{ v1 }}"
- debug: msg="role_1a v1 = {{ v1 }}"
- import_role:
    name: role_2
  vars:
    root: "role_1a"

gives as expected

    "msg": "role_1a v1 = role_1a default"
    "msg": "root = role_1a | role_2 v1 = role_1a default"
    "msg": "root = role_1a | role_3 v1 = role_1a default"
    "msg": "root = role_1a | role_4 v1 = role_1a default"
    "msg": "root = role_1a | role_5 v1 = role_1a default"
    "msg": "role_1b v1 = role_1a default"
    "msg": "root = role_1b | role_2 v1 = role_1a default"
    "msg": "root = role_1b | role_3 v1 = role_1a default"
    "msg": "root = role_1b | role_4 v1 = role_1a default"
    "msg": "root = role_1b | role_5 v1 = role_1a default"

because once the variable v1 was defined in the scope of the paybook there is no need to look for the default. It should be noted that

- debug: msg="role_1a v1 = {{ v1 }}"

does not define the variable v1. This statement will only evaluate the variable v1 on demand. Quoting from Ansible Glosary:

Lazy Evaluation: In general, Ansible evaluates any variables in playbook content at the last possible second, which means that if you define a data structure that data structure itself can define variable values within it, and everything “just works” as you would expect. This also means variable strings can include other variables inside of those strings.

The same result should be achieved with the changed order of the roles because all roles are imported.

- hosts: localhost
  roles:
    - role_1b
    - role_1a

gives

    "msg": "role_1b v1 = role_1b default" <=== expected role_1a default
    "msg": "root = role_1b | role_2 v1 = role_1b default" <=== expected role_1a default
    "msg": "root = role_1b | role_3 v1 = role_1b default" <=== expected role_1a default
    "msg": "root = role_1b | role_4 v1 = role_1a default"
    "msg": "root = role_1b | role_5 v1 = role_1a default"
    "msg": "role_1a v1 = role_1a default"
    "msg": "root = role_1a | role_2 v1 = role_1a default"
    "msg": "root = role_1a | role_3 v1 = role_1a default"
    "msg": "root = role_1a | role_4 v1 = role_1a default"
    "msg": "root = role_1a | role_5 v1 = role_1a default"

This shows that "namespasing" is probably needed all around the roles. For example

# roles/role_1b/tasks/main.yml
- set_fact:
    v1: "{{ v1 }}"
- debug: msg="role_1b v1 = {{ v1 }}"
- import_role:
    name: role_2
  vars:
    root: "role_1b"

works around the problem.

Related