Lookup and Filter Plugins¶
What You'll Learn¶
- The difference between a lookup (data in) and a filter (data transformed)
- The built-in lookups you'll use constantly, and
lookup()vs.query() - How to pull secrets from HashiCorp Vault or AWS Secrets Manager at run time
- When lookups run, and why that matters for performance and random values
- How to write a custom lookup plugin
Why This Exists¶
A loop: needs data from somewhere before it can iterate — lookup plugins pull data in (a file, an environment variable, an external secret manager); filter plugins (covered day-to-day in Filters and Tests) transform data already in hand.
Mental Model¶
Lookups run on the control node. A
lookup('file', 'x')readsxfrom your machine or CI runner, never from the managed host. Facts come from the managed node. If you need a file's content from a remote host, that'sslurporfetch, not a lookup.
| Question | Tool | Runs on |
|---|---|---|
| "What's in this file in my repo?" | lookup('ansible.builtin.file', ...) |
Control node |
| "What's in this file on the server?" | ansible.builtin.slurp module |
Managed node |
| "What OS is the server running?" | Facts | Managed node |
| "Reshape this list I already have" | Filter | Control node |
Built-In Lookups¶
vars:
# Read a file from the playbook/role (files/ search path)
ssh_banner: "{{ lookup('ansible.builtin.file', 'files/banner.txt') }}"
# Read an environment variable on the control node
ci_commit: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_SHA', default='local') }}"
# Run a command on the control node and use its output
git_tag: "{{ lookup('ansible.builtin.pipe', 'git describe --tags --abbrev=0') }}"
# Render a Jinja2 template to a string (not to a file)
motd: "{{ lookup('ansible.builtin.template', 'templates/motd.j2') }}"
# Generate a password once and store it locally for reuse
grafana_admin_password: "{{ lookup('ansible.builtin.password', 'credentials/grafana_admin length=32 chars=ascii_letters,digits') }}"
Loop over files matching a pattern:
- name: Install every public key in files/keys
ansible.posix.authorized_key:
user: deploy
key: "{{ lookup('ansible.builtin.file', item) }}"
loop: "{{ query('ansible.builtin.fileglob', 'files/keys/*.pub') }}"
lookup() vs. query()¶
lookup() returns a comma-joined string by default; query() (or q()) always returns a list. Use query inside loop: so a single result doesn't turn into a string of characters to iterate over.
Pulling Secrets at Run Time¶
Encrypting secrets with Ansible Vault still commits ciphertext to Git. A lookup against an external secret manager keeps the value out of the repository entirely.
HashiCorp Vault¶
- name: Configure the database client
ansible.builtin.template:
src: db.env.j2
dest: /etc/checkout/db.env
mode: "0600"
vars:
db_secret: >-
{{ lookup('community.hashi_vault.vault_kv2_get', 'checkout/database',
engine_mount_point='secret',
url='https://vault.internal.example.com:8200',
auth_method='approle',
role_id=vault_role_id,
secret_id=vault_secret_id) }}
db_password: "{{ db_secret.secret.password }}"
no_log: true
AWS Secrets Manager¶
vars:
db_password: "{{ lookup('amazon.aws.secretsmanager_secret', 'prod/checkout/db-password', region='us-east-1') }}"
Credentials come from the control node's normal AWS credential chain — an instance role or CI OIDC role, not keys in the playbook.
In both cases, mark consuming tasks no_log: true so the value never reaches logs.
When Lookups Run¶
Lookups in vars: are lazy: they're evaluated each time the variable is used, not once.
-
A
pipeor secret-manager lookup referenced in ten tasks runs ten times, on every host that uses it. For expensive calls, resolve once and store it: -
A lookup that returns a different value each time (a random value) will differ between tasks. The
passwordlookup avoids this by writing the value to a file on first use and reading it back afterwards.
Filters Recap¶
Filters transform data you already have and also run on the control node:
The everyday set is in Filters and Tests, and writing your own is in Advanced Jinja2.
Writing a Custom Lookup Plugin¶
When data lives in an internal system with an HTTP API, a lookup plugin is cleaner than repeated uri calls.
DOCUMENTATION = r"""
name: cmdb_owner
short_description: Look up a service owner in the internal CMDB
options:
_terms:
description: Service names to look up.
required: true
api_url:
description: CMDB base URL.
default: https://cmdb.internal.example.com
env:
- name: CMDB_API_URL
"""
import json
from ansible.errors import AnsibleLookupError
from ansible.module_utils.urls import open_url
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
self.set_options(var_options=variables, direct=kwargs)
base = self.get_option("api_url")
results = []
for service in terms:
try:
resp = open_url(f"{base}/api/services/{service}", timeout=10)
results.append(json.load(resp)["owner"])
except Exception as exc:
raise AnsibleLookupError(f"cmdb_owner: could not look up {service}: {exc}")
return results
- ansible.builtin.debug:
msg: "checkout is owned by {{ lookup('acme.platform.cmdb_owner', 'checkout') }}"
run() must always return a list. Place the file in lookup_plugins/ next to a playbook, or in plugins/lookup/ of a collection (as above, called by FQCN) — see Build a Collection From Zero.
Common Mistakes¶
- Confusing a lookup (control-node-side, e.g. reading a local file) with a fact (managed-node-side data) — they answer different questions about different machines.
- Using
lookup('pipe', ...)to shell out for something a real module or filter already does. - Using
lookup()in aloop:and iterating over the characters of a comma-joined string — usequery(). - Putting an expensive or rate-limited lookup in
group_varsand calling the API hundreds of times per run. - Pulling a secret with a lookup but forgetting
no_log: trueon the task that uses it.
Interview Questions¶
- What's the difference between a lookup plugin and a fact, in terms of which machine the data comes from?
- How would you pull a secret from an external vault without ever committing it to the playbook repository?
- What's the difference between
lookup()andquery()? - Why can a lookup in
vars:run many more times than you expect?
Next¶
Continue to Dynamic Inventory.