Open-Source Wikis

/

Ansible

/

Plugins

/

Lookup plugins

ansible/ansible

Lookup plugins

Lookups are controller-side functions that produce data into the variable namespace. They're invoked as {{ lookup('name', ...) }} (or with the shorter alias {{ q('name', ...) }}). Used to read files, environment variables, vault contents, command output, password files, or anything else the controller can produce.

Where they live

lib/ansible/plugins/lookup/
├── __init__.py
├── config.py
├── csvfile.py
├── dict.py
├── env.py
├── file.py
├── fileglob.py
├── first_found.py
├── indexed_items.py
├── ini.py
├── ... (about 40 in total)

A non-exhaustive list of built-ins: config, csvfile, dict, env, file, fileglob, first_found, indexed_items, ini, items, lines, list, nested, password, pipe, random_choice, sequence, subelements, template, together, unvault, url, vars, with_dict, with_first_found, with_items, etc.

Lookups vs. filters

  • A filter transforms an existing value: {{ value | upper }}.
  • A lookup produces data: {{ lookup('file', '/etc/hosts') }}.

The distinction is functional: lookups can read files, run subprocesses, hit the network. Filters should not.

Lookup vs. with_

Old syntax loops used with_<lookup> directly:

- debug: msg={{ item }}
  with_items: '{{ my_list }}'

Modern form uses loop: with lookup or just bare data:

- debug: msg={{ item }}
  loop: '{{ my_list }}'

Both still work, but with_* keywords are essentially aliases for loop: "{{ lookup('<name>', ...) }}" with a wantlist flag.

LookupBase

lib/ansible/plugins/lookup/__init__.py:LookupBase is the parent. The contract:

class LookupModule(LookupBase):
    def run(self, terms, variables=None, **kwargs):
        # terms: the positional args, always a list (the first one is normally the "input")
        # variables: the current variable scope
        # kwargs: keyword arguments parsed from k=v style
        return [list_of_results]

The return value must be a list even for "single-value" lookups (the result of lookup('env', 'HOME') is [home_path], and the templating layer unwraps the single-element list). Use wantlist=True to keep it as a list when the user expects multiple results.

The base provides:

  • _loader — a DataLoader for reading files relative to the playbook.
  • find_file_in_search_path(...) — locate files/foo.txt correctly in role search order.
  • get_basedir(variables) — the playbook root for relative paths.
  • set_options(...) and get_option(...) — for plugin options.

Common patterns

file lookup

- debug: msg="{{ lookup('file', '/etc/hostname') }}"

lib/ansible/plugins/lookup/file.py reads the file with the dataloader (so vault-encrypted files decrypt automatically). Returns the file's text content.

env lookup

- debug: msg="API key starts with {{ lookup('env', 'API_KEY')[:8] }}"

lib/ansible/plugins/lookup/env.py returns os.environ.get(name, ''). Note this reads the controller's environment, not the target's.

pipe lookup

- name: who am I locally
  debug: msg="{{ lookup('pipe', 'whoami') }}"

Runs the command on the controller and returns stdout. Used for "fetch this small piece of dynamic data" cases.

password lookup

- name: ensure user has a password
  user:
    name: alice
    password: "{{ lookup('password', '/path/to/password.txt length=20 chars=ascii_letters') }}"

Reads the file if it exists, generates a new password if not. Stable across runs — the lookup file is the source of truth.

template lookup

- debug: msg="{{ lookup('template', 'config.yml.j2') }}"

Renders a Jinja template against the current variable scope and returns the result. Used for inlining computed config snippets.

first_found

- copy:
    src: "{{ lookup('first_found', files) }}"
    dest: /etc/foo.conf
  vars:
    files:
      - 'config.{{ ansible_distribution }}.conf'
      - 'config.default.conf'

Walk a list of paths and return the first one that exists. Bigger than it sounds — the first_found plugin handles the role search path semantics.

unvault

lib/ansible/plugins/lookup/unvault.py decrypts a vault file and returns its contents. Used when you want to read encrypted credentials inline rather than putting them in group_vars/all/vault.yml.

How lookups integrate with templating

graph LR
    EXPR["{{ lookup('file', path) }}"] --> JINJA[AnsibleEnvironment]
    JINJA --> INVOKE[_invoke_lookup<br/>_internal/_templating/_jinja_plugins.py]
    INVOKE --> LOAD[lookup_loader.get name]
    LOAD --> CLASS[LookupModule]
    CLASS --> RUN[run(terms, variables)]
    RUN --> RESULT[Return list]
    RESULT --> UNWRAP{wantlist?}
    UNWRAP -->|no| ELEM[Return first element]
    UNWRAP -->|yes| LIST[Return entire list]

The lookup runs synchronously inside the templating evaluation. Slow lookups slow down templating, which slows down task dispatch. Cache aggressively: lookup('url', ...) makes a fresh HTTP call every time, but you can wrap it in set_fact once.

Errors and undefined

Lookups can fail. Standard convention:

  • A required term that's missing → raise AnsibleError("...").
  • A term that can't be found but is okay to skip → return [].
  • A connection or permission error → raise.

Errors propagate up through Jinja and surface as a templating error with the lookup name attached.

Integration points

  • Loaded by: lookup_loader in lib/ansible/plugins/loader.py.
  • Invoked from: lib/ansible/_internal/_templating/_jinja_plugins.py:_invoke_lookup.
  • Used by: any Jinja expression in a playbook, vars file, or template.

Entry points for modification

  • A new lookup — write a plugin in a collection. Subclass LookupBase. Implement run(). Declare options in DOCUMENTATION.
  • Adding q() / query() syntax — already done; q is registered as an alias by _jinja_plugins.py. Don't add a new top-level alias without consensus.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Lookup plugins – Ansible wiki | Factory