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