Ansible runs task in role even when condition is false
I ran into a perplexing problem where Ansible was running a task in a role even if the role had a when condition which was resolving to false. Plus the task was failing.
To visualize it better, one task (not all) was running in the role symptom in the example playbook below,
--- - hosts: all roles: - role: symptom when: - false
The code that reproduces the issue is below,
./play.yml --- - hosts: all roles: - role: symptom when: - false ./roles/symptom/tasks/main.yml --- - name: create temp directory file: path: /tmp/blog state: directory - name: create directory structure file: path: "/tmp/blog/{{ item }}" state: directory loop: "{{ ['one', 'two', 'three', 'four'] }}" - name: get directories find: paths: /tmp/blog recurse: yes file_type: directory register: reg_dirs - name: must not run when role is ignored file: path: "{{ item }}/test" state: present loop: "{{ reg_dirs | json_query('file.[*]') }}" ./inventory all: children: localhost: hosts: localhost: ansible_connection: local ./ansible.cfg [defaults] become_allow_same_user = yes inventory = ./inventory
In the code above, task "must not run when role is ignored" was being
executed and failing with message "Invalid data passed to 'loop', it requires a
list, got this instead: . Hint: If you passed a list/dict of just one element,
try adding wantlist=True to your lookup invocation or use q/query instead of
lookup." When you look at play.yml, the role has a condition of false. How
could this be? Hint: the line loop: "{{ reg_dirs | json_query('file.[*]') }}"
is significant, where variable reg_dirs is being evaluated and processed.
$ ansible-playbook play.yml [WARNING]: Found both group and host with same name: localhost PLAY [all] *********************************************************************************************************************************************** TASK [Gathering Facts] *********************************************************************************************************************************** [WARNING]: Platform darwin on host localhost is using the discovered Python interpreter at /usr/local/bin/python3.11, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.15/reference_appendices/interpreter_discovery.html for more information. ok: [localhost] TASK [roles/symptom : create temp directory] ******************************************************************************************************** skipping: [localhost] TASK [roles/symptom : create directory structure] ******************************************************************************************************** skipping: [localhost] => (item=one) skipping: [localhost] => (item=two) skipping: [localhost] => (item=three) skipping: [localhost] => (item=four) skipping: [localhost] TASK [roles/symptom : get directories] ******************************************************************************************************************* skipping: [localhost] TASK [roles/symptom : must not run when role is ignored] ************************************************************************************************* fatal: [localhost]: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list, got this instead: . Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."} PLAY RECAP *********************************************************************************************************************************************** localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0
Also notice that all tasks were skipped except the one with loop:
.
This is the symptom reported by someone else, "Tasks inside a block that use a loop are not skipped even if the block's when condition evaluates to false". The real nugget was a reply in the post, "The meaning of that, is that the when statement is evaluated for each iteration of the loop, and not before the loop. As a result, your loop must always evaluate as an iterable, regardless of the when statement."
I tried to follow this advice but it wasn't working. I realized I had one more
aspect in my Ansible code: the loop was using a variable that was created as
part of register:
from a previous task.
It appeared that variables set in the following way did not have an impact in my case,
- vars/main.yml
- inventory
- passed with
--extra-vars
- set_fact
But a variable created during a task with register:
was causing my issue.
I speculate that in the former case Ansible knows the value of the variable
when it is used in loop:
. But the combination of a variable where the value
is unknown until the task (where it is registered) runs and processing its
value in the loop causes this issue. As loop is evaluated before the when
condition it kind of makes sense.
With the above assumption, I moved the processing of
"{{ reg_dirs | json_query('file.[*]') }}"
before the loop task into a
set_fact
. This modified my assumption that Ansible knows the value of
set_fact, because obviously it doesn't. I changed my assumption to that Ansible
does not evaluate the value of set_fact like it does for loop:
until later
in the execution. In other words, any evaluations in loop:
are done earlier
in the execution but evaluation in set_fact:
are much later.
Making this change solved my issue.
./play.yml --- - hosts: all roles: - role: fixed when: - false ./roles/fixed/tasks/main.yml --- - name: create temp directory file: path: /tmp/blog state: directory - name: create directory structure file: path: "/tmp/blog/{{ item }}" state: directory loop: "{{ ['one', 'two', 'three', 'four'] }}" - name: get directories find: paths: /tmp/blog recurse: yes file_type: directory register: reg_dirs - name: set fact set_fact: list_dirs: "{{ reg_dirs | json_query('files[*].path') }}" - name: must not run when role is ignored file: path: "{{ item }}/test" state: present loop: "{{ list_dirs }}"
Notice that loop: "{{ reg_dirs | json_query('file.[*]') }}"
in the failing
case becomes two parts,
set_fact: list_dirs: "{{ reg_dirs | json_query('files[*].path') }}"
and
loop: "{{ list_dirs }}"
. Problem solved.
$ ansible-playbook play.yml [WARNING]: Found both group and host with same name: localhost PLAY [all] *********************************************************************************************************************************************** TASK [Gathering Facts] *********************************************************************************************************************************** [WARNING]: Platform darwin on host localhost is using the discovered Python interpreter at /usr/local/bin/python3.11, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.15/reference_appendices/interpreter_discovery.html for more information. ok: [localhost] TASK [roles/fixed : create temp directory] ********************************************************************************************************** skipping: [localhost] TASK [roles/fixed : create directory structure] ********************************************************************************************************** skipping: [localhost] => (item=one) skipping: [localhost] => (item=two) skipping: [localhost] => (item=three) skipping: [localhost] => (item=four) skipping: [localhost] TASK [roles/fixed : get directories] ********************************************************************************************************************* skipping: [localhost] TASK [roles/fixed : set fact] **************************************************************************************************************************** skipping: [localhost] TASK [roles/fixed : must not run when role is ignored] *************************************************************************************************** skipping: [localhost] PLAY RECAP *********************************************************************************************************************************************** localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=5 rescued=0 ignored=0