3
votes

My goal is to have a flexible Ansible role where the value of a variable can be provided in this order of precedence (greatest to least):

  1. from the command line using '--extra-vars'
  2. from environment variables
  3. from the defaults in the role

1 & 3 match the order of precedence documented for Ansible variables (http://docs.ansible.com/ansible/latest/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable), so I have focused my attention on the environment variables. Using the lookup('env',...) plugin, I am able to read in the environment variable in the vars/main.yml and the precedence order is exactly what I want.

However, when the environment variable is not defined, the lookup plugin returns an empty string. This means that the variable is assigned the empty string instead of remaining undefined so that the default value can be assigned.

Playbook (var-exp.yml)

- name: Variable experiment
  hosts: all

  tasks:
  - import_role:
      name: ansible-role-variable-experiment

Role (ansible-role-variable-experiment)

tasks/main.yml

- name: Display value of 'location'
  debug:
    msg: 'location is {{ location }}'

defaults/main.yml

location: from-defaults-main-yml

vars/main.yml

# If used, this will override the value in defaults/main.yml, as expected
# location: from-vars-main-yml

# Since lookup returns '' when the environment variable doesn't exist,
# location gets set to '' instead of being left undefined so that the
# default can be used:
# location: "{{ lookup('env', 'LOCATION' ) }}"  # -> location == ''

# When the environment variable does not exist, all of these options generate 
# some value assigned to location so that the default cannot be assigned:
# location: "{{ lookup('env', 'LOCATION' ) | default(None, true) }}"  # -> location == ''
# location: "{{ lookup('env', 'LOCATION' ) | default(omit, true) }}"  # -> location == __omit_place_holder__3e8bdbb6cebc653a758afca99607fcf9ec1f99f4
# location: "{{ lookup('env', 'LOCATION' ) | default('undefined') }}"  # -> location == ''
# location: "{{ lookup('env', 'LOCATION' ) | reject('undefined') }}"  # -> location == <generator object select_or_reject at 0x10caaef00>

# When the environment variable does not exist, these generate a recursive 
# loop that crashes the play:
# location: "{{ lookup('env', 'LOCATION' ) | default(location) }}"
# location: "{{ lookup('env', 'LOCATION' ) | default(location, true) }}"

Execution Examples

With environment variable:

LOCATION=from-env-variable ansible-playbook ./var-exp.yml

Without environment variable:

ansible-playbook ./var-exp.yml

I have not been able to identify a clean way to accomplish my goal. I have been able to come up with a way to "work around" this:

vars/main.yml

default_location: from-default-array-in-vars-main-yml

location: "{{ lookup('env', 'LOCATION' ) | default(default_location, true) }}"

And while this seems to accomplish what I want, now I am defining "defaults" in the "vars" area instead of in "defaults" area.

The Ansible documentation says something like "if you are doing something that seems too complicated, it probably is."

So, my question is: Is there an easier/better/correct way to accomplish this? Or, have I run into a limitation in the way Ansible 2.4.2 (or the lookup('env') plugin) is currently implemented?

1

1 Answers

2
votes

There is no way to do it using a single variable name.

The only workaround I can think of is using set_fact:

- set_fact:
    location: "{{ lookup('env', 'LOCATION' ) | ternary (lookup('env', 'LOCATION' ), omit) }}"

This way if environment variable LOCATION does not exist/is empty, the task will not assign a value (omit) and the default from the role will be used (i.e. it will not get overridden).

You could either run place it in pre_tasks before roles are called, or at the top of roles tasks/main.yml


Rationale:

The whole problem you brought is using a single variable name throughout the process, but:

  • variables in Ansible are lazy-evaluated
  • once you define a variable in the vars section, it has precedence over the one defined in defaults (even though evaluation will take place later - lookup plugin is irrelevant to the problem)
  • there is no way to undefine variable in Ansible

Ansible processes the "precedence chain" for variables (before running any task) and once it encounters a definition, it stops. There is no further "going back" at execution time, when the actual values are evaluated.


Remark:

 I have been able to come up with a way to "work around" this:

[ ]

now I am defining "defaults" in the "vars" area instead of in "defaults" area.

But you don't have to. There is no requirement to put your default_location variable into vars. You can define it in defaults (the value is used at the execution time).