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 }}"

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.