Skip to main content
RunBook Academy

AnsibleLI · Custom Modules and Tool SelectionChoosing the right tool

Filter, lookup, module or action plugin

Advanced⏱ ~28 minbash

What you'll learn

  • Decide between a filter, a lookup, a module and an action plugin from where the code must execute
  • Predict which host a given piece of plugin code reads from
  • Recognise the failure signature of a lookup used where a module was needed
  • Choose the cheapest plugin type that can answer the requirement

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Ansible has around twenty plugin types. Four of them get written by users, and choosing between them looks like a taxonomy problem.

It is not. It is one question:

Does this code need to run on the controller, or on the managed node?

Answer that and the type follows. Get it wrong and you produce automation that works perfectly on the developer’s laptop, works perfectly in a play targeting localhost, and reads the wrong machine in production — while reporting success.

The four types, by where they execute

TypeRuns onSeesCan change the target
FilterControllerData you passed itNo
LookupControllerThe controller’s filesystem, environment, networkNo
ModuleManaged nodeThe managed node’s filesystem, processes, servicesYes
Action pluginController, and usually dispatches a module to the nodeBoth, deliberatelyYes, via the module it dispatches

The first two are transformations and reads. The third is the only one that acts on the host. The fourth is the seam between them, and it is the one you almost never need.

The demonstration

A play targeting two remote hosts, with a lookup reading a file.

Read-only / Safelookupdemo.yml
- name: A lookup in a play targeting remote hosts
hosts: web01.example.com,web02.example.com
gather_facts: false
tasks:
  - name: Read a file with a lookup
    ansible.builtin.debug:
      msg: "{{ lookup('file', playbook_dir + '/marker.txt') }}"
Read-only / Safethe result against two unreachable hosts
$ ansible-playbook -i inventory/hosts.yml lookupdemo.yml
TASK [Read a file with a lookup] ***********************************************
ok: [web01.example.com] => {
  "msg": "controller-copy"
}
ok: [web02.example.com] => {
  "msg": "controller-copy"
}

PLAY RECAP *********************************************************************
web01.example.com          : ok=1    changed=0    unreachable=0    failed=0
web02.example.com          : ok=1    changed=0    unreachable=0    failed=0

Read that recap carefully. Two hosts that do not exist, that no packet ever reached, both reported ok=1 and unreachable=0. The lookup ran twice on the controller — once per host in the play — and read the controller’s file both times.

The correct form, when the file is genuinely on the managed node:

Read-only / Safereading a remote file with a module
- name: Read the file from the managed node
ansible.builtin.slurp:
  src: /etc/app/version
register: remote_version

- name: Use it
ansible.builtin.debug:
  msg: "{{ remote_version['content'] | b64decode | trim }}"

# Or, when you only need to know whether it exists:
- name: Check for the marker
ansible.builtin.stat:
  path: /etc/app/version
register: marker

slurp returns base64 because the file may contain anything, including bytes that are not valid text. The | b64decode is not ceremony; it is the price of a module that can read any file.

Choosing, in order of cost

Work down. Stop at the first that fits.

  1. Is it a pure transformation of data you already have? A filter. Cheapest by a wide margin: a plain Python function, no state, no idempotency, no changed value, no check mode.
  2. Does it read something on the controller - a file, an environment variable, a secret manager, an API the controller can reach? A lookup. Still controller-side, still no change semantics.
  3. Does it need to inspect or modify the managed node? A module. Now you owe the full contract from lesson 3.
  4. Does it need to do controller-side work AND dispatch a module - copy a file into place before running something, or template on the controller then transfer? An action plugin. Read the built-in ones first; template and copy are action plugins and they are how you learn the pattern.
  5. If two answers seem to fit, prefer the one further up this list. The cost difference between a filter and a module is not small.

Two cases that look ambiguous and are not

“It calls an API, so where does it run?” Whichever side has the network route and the credentials. Most APIs are reachable from the controller, so uri or a lookup is correct and no managed node is involved at all — which is why so many cloud modules are effectively controller-side work with delegate_to: localhost. If the API is only reachable from the managed node, it must be a module.

“It reads a file, so it must be a lookup.” Only if the file is on the controller. This is the confusion the demonstration above exists to break, and the discriminating question is never “what does the code do” but “which machine’s disk holds the bytes”.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A play targets 300 web servers and uses `{{ lookup("file", "/etc/app/version") }}` to read each host's installed version. What actually happens?

  2. Q2. Which properties make a lookup used in place of a module hard to catch in review or testing? Select all that apply.

  3. Q3. ansible.builtin.template is an action plugin: it renders on the controller and then transfers the result, which is why the managed node needs no Jinja installed.

  4. Q4. The requirement is "given a list of interface dictionaries, produce the subset that have an IPv6 address". Which plugin type?

  5. Q5. A team needs to parse a vendor configuration format both on the controller and on managed nodes. What is the best structure?

Passing score: 75%. Answers are checked in this browser.