Env Lookup — Reading Environment Variable
Same lesson as the previous chapter, applied to a different source: environment variables. And it’s worth the repetition, because this is exactly the kind of thing that’s easy to forget the moment a second lookup plugin enters the picture.
Reading An Environment Variable With lookup('env', ...)
msg: "{{ lookup('env', 'HOME') }}"Whose Environment, Exactly?
Exactly the rule from the previous chapter, restated for env specifically: this reads the control node’s environment — the environment of the actual process running ansible-playbook — never the remote managed host’s environment, regardless of which host a task appears to be running against.
Proving It Concretely (Again)
- name: Compare Environments
hosts: servers
tasks:
- name: Show the remote connection user, from inventory
ansible.builtin.debug:
msg: "Connecting as: {{ ansible_user }}"
- name: Show the control node's own USER environment variable
ansible.builtin.debug:
msg: "Control node USER: {{ lookup('env', 'USER') }}"ok: [ubuntu] => { "msg": "Connecting as: alice" }
ok: [ubuntu] => { "msg": "Control node USER: you" }
ok: [fedora] => { "msg": "Connecting as: alice" }
ok: [fedora] => { "msg": "Control node USER: you" }ansible_user correctly shows alice — the account Ansible connects as over SSH. lookup('env', 'USER') shows whoever is actually logged into the control node running this playbook, completely unrelated to alice, and identical across both hosts for exactly the same reason the previous chapter’s /etc/hostname example was.
A Realistic Use: Reading A Secret From The Control Node’s Environment
A genuinely common pattern: keeping a credential out of a vars file or version control entirely, stored instead as an environment variable on the machine running Ansible:
vars:
api_key: "{{ lookup('env', 'MY_API_KEY') }}"
tasks:
- name: Use the API key
ansible.builtin.debug:
msg: "Using key starting with: {{ api_key[:4] }}..."(Full secrets management — Ansible Vault, properly encrypting sensitive values — is its own topic for later; this is a lighter-weight option worth knowing about in the meantime.)
A Gotcha: An Undefined Environment Variable Returns An Empty String, Not An Error
Unlike a genuinely undefined Jinja2 variable, an environment variable that was never set doesn’t trigger is undefined or fail mandatory — lookup('env', ...) simply returns an empty string, silently:
msg: "Value: [{{ lookup('env', 'DEFINITELY_NOT_SET_ANYWHERE') }}]"Value: []No error, nothing to catch with the tools from the tests and conditionals section — the value is technically defined, it just happens to be empty. A required secret that was never actually exported on the control node fails silently this way, and anything downstream assuming it “must have worked” proceeds with a blank value and no warning at all.
You can either set default value (default="value"):
msg: "Hello {{ lookup('env', 'UNDEFINED_VARIABLE', default='Default Value' }}"Or fail if undefined (default=undef()):
msg: "Hello {{ lookup('env', 'UNDEFINED_VARIABLE', default=undef()) }}"You can even stop the play:
- name: Fail clearly if the secret wasn't actually set
ansible.builtin.fail:
msg: "MY_API_KEY environment variable is not set on the control node"
when: api_key | length == 0ansible.builtin.fail stops the play immediately with a custom message — exactly the loud, clear failure this situation deserves, instead of a silently blank credential quietly propagating further into the playbook.
Best Practices
- Remember
lookup('env', ...)always reads the control node’s environment — the same rule as every lookup in this section, with no exceptions. - Check for emptiness explicitly (
| length == 0), notis undefinedormandatory, when validating a value sourced fromenv— a missing environment variable is defined and empty, not undefined. - Use
ansible.builtin.failwith a clear message when a required environment-sourced value turns out to be missing, rather than letting a blank value propagate silently.