Ansible Dysfunction #1: undefined variables break unused roles

I love what Ansible is capable of. Because of this, I use it extensively in my daily work - and this has led to my being in possession of a slowly growing catalogue of what we might politely call the "quirks" of Ansible. Publicly cataloguing them may or may not help others, but it should at least make me feel better for venting. (Ansible 2.3.x - confirmed on Fedora Core and Mac.)

If you have a variable in a role, defined by a set_fact: and then called, the role will run fine. But if you include that role in a playbook and then skip the role via tags, it will break the playbook. That's right: the fact that a variable is undefined in a part of the playbook that is not going to be used will break Ansible.

Here's the code to prove it, starting with the undefinedvar.yml playbook:

---
# undefinedvar.yml

- name: skip setting a variable
  hosts: localhost
  roles:
    - { role: undefinedvar, tags: undefinedvar }
    - { role: dummy,        tags: dummy        }

The roles/undefinedvar/tasks/ folder includes three files.

---
# roles/undefinedvar/tasks/main.yml

# known to work with Bash 3 and 4
- name: get the Bash major release number
  shell: "bash --version | head -n 1 | awk '{ print $4 }' | awk -F '.' '{ print $1 }'"
  register: bash_ver
  changed_when: False

- name: include a file based on the Bash major release number
  include: 'bash{{ bash_ver.stdout }}.yml'
---
# roles/undefinedvar/tasks/bash3.yml

- name: announce ourselves
  debug:
    msg: 'You are in the file bash3.yml'
---
# roles/undefinedvar/tasks/bash4.yml

- name: announce ourselves
  debug:
    msg: 'You are in the file bash4.yml'

And finally the dummy/tasks/main.yml file:

---
# roles/dummy/tasks/main.yml

- name: show us where we are and we're done
  debug:
    msg: "You're in the 'dummy' role, and this is its only action."

Ansible output:

giles:~..e/Ansible/tests$ ansible-playbook undefinedvar.yml
 [WARNING]: provided hosts list is empty, only localhost is available


PLAY [skip setting a variable] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [undefinedvar : get the Bash major release number] ************************
ok: [localhost]

TASK [undefinedvar : include a file based on the Bash major release number] ****
included: /home/giles/Code/Ansible/tests/roles/undefinedvar/tasks/bash4.yml for localhost

TASK [undefinedvar : announce ourselves] ***************************************
ok: [localhost] => {
    "msg": "You are in the file bash4.yml"
}

TASK [dummy : show us where we are and we're done] *****************************
ok: [localhost] => {
    "msg": "You're in the 'dummy' role, and this is its only action."
}

PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0

giles:~..e/Ansible/tests$ ansible-playbook undefinedvar.yml --tags dummy
 [WARNING]: provided hosts list is empty, only localhost is available


PLAY [skip setting a variable] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [undefinedvar : include a file based on the Bash major release number] ****
fatal: [localhost]: FAILED! => {"failed": true, "msg": "'bash_ver' is undefined"}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1   

giles:~..e/Ansible/tests$

It runs correctly if you run the entire playbook. But then I asked it to run only the "dummy" role with the --tags parameter. And it barfed on an undefined variable in the other role.

This can be worked around by setting a default value in the roles/undefinedvar/vars/main.yml for the role, although this may also have undesirable side effects.