Introduction
Lookup plugins read data from external sources on the control node (not the remote host). Read files, environment variables, command output, passwords, and more — then use that data in playbooks and templates.
Syntax
# lookup function — returns a string
"{{ lookup('file', '/etc/hostname') }}"
# query function — returns a list
"{{ query('file', '/etc/hostname') }}"
# In tasks
- debug:
msg: "{{ lookup('env', 'HOME') }}"
Common Lookup Plugins
file — Read a File
# Read file content
- name: Load SSH key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
# Read into variable
- set_fact:
ssl_cert: "{{ lookup('file', 'files/server.crt') }}"
env — Environment Variables
# Read environment variable
- debug:
msg: "Home: {{ lookup('env', 'HOME') }}"
- debug:
msg: "User: {{ lookup('env', 'USER') }}"
# Use in variables
vars:
api_key: "{{ lookup('env', 'API_KEY') }}"
deploy_env: "{{ lookup('env', 'DEPLOY_ENV') | default('production') }}"
pipe — Command Output
# Run command on control node, get output
- debug:
msg: "Date: {{ lookup('pipe', 'date +%Y-%m-%d') }}"
- debug:
msg: "Git hash: {{ lookup('pipe', 'git rev-parse HEAD') }}"
- set_fact:
current_user: "{{ lookup('pipe', 'whoami') }}"
password — Generate or Read Passwords
# Generate random password (saved to file)
- name: Set database password
ansible.builtin.set_fact:
db_password: "{{ lookup('password', '/tmp/db_password length=32 chars=ascii_letters,digits') }}"
# Generate without saving to file
- set_fact:
random_token: "{{ lookup('password', '/dev/null length=64 chars=hexdigits') }}"
# Generate password with special chars
- set_fact:
secure_pass: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits,punctuation') }}"
template — Render a Template
# Render a Jinja2 template and use the result
- set_fact:
rendered_config: "{{ lookup('template', 'config.yml.j2') }}"
- name: Show rendered template
debug:
msg: "{{ lookup('template', 'motd.j2') }}"
fileglob — Find Files by Pattern
# Find all .conf files
- name: Copy all config files
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/myapp/conf.d/
loop: "{{ lookup('fileglob', 'files/conf.d/*.conf', wantlist=True) }}"
# Find all templates
- debug:
msg: "Templates: {{ lookup('fileglob', 'templates/*.j2', wantlist=True) }}"
csvfile — Read CSV Data
# users.csv
username,role,email
alice,admin,alice@example.com
bob,developer,bob@example.com
# Read from CSV
- debug:
msg: "Alice's role: {{ lookup('csvfile', 'alice file=users.csv delimiter=, col=1') }}"
- debug:
msg: "Bob's email: {{ lookup('csvfile', 'bob file=users.csv delimiter=, col=2') }}"
ini — Read INI Files
# config.ini
[database]
host=db.internal
port=5432
[cache]
host=redis.internal
- debug:
msg: "DB host: {{ lookup('ini', 'host section=database file=config.ini') }}"
- debug:
msg: "Cache host: {{ lookup('ini', 'host section=cache file=config.ini') }}"
url — Fetch from URL
# Read content from URL
- debug:
msg: "{{ lookup('url', 'https://api.ipify.org') }}"
# Fetch JSON
- set_fact:
api_data: "{{ lookup('url', 'https://api.example.com/config', headers={'Authorization': 'Bearer ' + token}) | from_json }}"
subelements — Nested Loops
vars:
users:
- name: alice
keys:
- ssh-ed25519 AAAA... alice@laptop
- ssh-ed25519 BBBB... alice@desktop
- name: bob
keys:
- ssh-ed25519 CCCC... bob@laptop
- name: Add all SSH keys for all users
ansible.builtin.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
loop: "{{ lookup('subelements', users, 'keys') }}"
dict — Iterate Dictionaries
vars:
packages:
nginx: present
redis: present
apache2: absent
- name: Manage packages
ansible.builtin.package:
name: "{{ item.key }}"
state: "{{ item.value }}"
loop: "{{ lookup('dict', packages) }}"
lookup vs query
# lookup — returns string (comma-separated for multiple results)
"{{ lookup('fileglob', 'files/*.conf') }}"
# Result: "a.conf,b.conf,c.conf"
# query — returns list (better for loops)
"{{ query('fileglob', 'files/*.conf') }}"
# Result: ["a.conf", "b.conf", "c.conf"]
# wantlist=True — force lookup to return list
"{{ lookup('fileglob', 'files/*.conf', wantlist=True) }}"
Use query() or lookup(..., wantlist=True) when looping.
Practical Patterns
Load Secrets from Environment
vars:
db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
api_key: "{{ lookup('env', 'API_KEY') }}"
aws_access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
- name: Validate secrets exist
assert:
that:
- db_password | length > 0
- api_key | length > 0
fail_msg: "Required environment variables not set"
Dynamic Inventory Data
# Read host list from file
- name: Add hosts dynamically
ansible.builtin.add_host:
name: "{{ item }}"
groups: dynamic_hosts
loop: "{{ lookup('file', 'hosts.txt').splitlines() }}"
Generate and Store Credentials
- name: Generate app secret
set_fact:
app_secret: "{{ lookup('password', 'credentials/app_secret length=64 chars=hexdigits') }}"
# Password file is created on control node
# Same lookup returns same password on subsequent runs
Troubleshooting
"File not found"
Lookups run on the control node, not the remote host. The path is relative to the playbook or role:
# Relative to playbook directory
"{{ lookup('file', 'files/myfile.txt') }}"
# Absolute path
"{{ lookup('file', '/etc/hostname') }}"
# In a role, relative to role's files/ directory
"{{ lookup('file', 'myfile.txt') }}"
lookup vs Fetching Remote Files
# lookup — reads from CONTROL NODE
"{{ lookup('file', '/etc/hostname') }}" # Control node's hostname!
# To read from REMOTE host, use slurp module
- slurp:
src: /etc/hostname
register: remote_hostname
- debug:
msg: "{{ remote_hostname.content | b64decode }}"
Related Articles
Conclusion
Lookup plugins read external data on the control node. Use file for files, env for environment variables, pipe for command output, password for credential generation, and template for rendering Jinja2. Remember: lookups run locally on the control node, not on remote hosts. Use query() instead of lookup() when you need a list for looping. Combine lookups with Vault for secure secret management.