Ansible Pitfall: Accessing Variables using vars

tl;dr: vars is an undocumented internal variable with surprising behaviour. Use foo instead of vars['foo'] and lookup('vars', 'foo' ~ bar) instead of vars['foo' ~ bar].

For a long time the only supported way of retrieving variables based on an expression was to use hostvars. This generally works, but not all variables that are in scope are present in hostvars. At some point people learned that you could use a dictionary called vars to access more variables than are present in hostvars[inventory_hostname]:

- hosts: localhost
  vars:
    foo: pisces
  tasks:
    - debug:
        msg: "{{ vars['foo'] }}"

    - debug:
        msg: "{{ hostvars[inventory_hostname]['foo'] }}"
TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "pisces"
}

TASK [debug] *******************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'foo'\n\nThe error appears to be in '/home/ec2-user/test.yml': line 9, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - debug:\n      ^ here\n"}

The problem is that this variable was never intended to be used this way. As such, it exhibits some surprising behaviour that you might not encounter in simple tests but will definitely trip you up at some point. For instance, values in vars are not templated:

- hosts: localhost
  vars:
    foo: pisces
    bar: "{{ foo[0:2] }}"
  tasks:
    - debug:
        msg: "{{ bar }}"

    - debug:
        msg: "{{ vars['bar'] }}"
TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "pi"
}

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "{{ foo[0:2] }}"
}

Because people are using this in the wild the Ansible developers have not removed this variable, but they have expressed a desire to do so once variable deprecation is implemented. In its place they have provided the vars lookup, which can access all variables that are in scope and does not exhibit any of the unexpected behaviour of the vars variable.

- hosts: localhost
  vars:
    foo: pisces
    bar: "{{ foo[0:2] }}"
  tasks:
    - debug:
        msg: "{{ lookup('vars', 'b' ~ 'ar') }}"
TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "pi"
}