AnsibleVIII · Modules and the Module ModelThe module model
How modules fail, and what the error means
What you'll learn
- Identify the five recurring module failure classes from their first lines
- Sort a failure into a controller problem or a managed-node problem before investigating
- Diagnose an unresolvable module name when the error cannot distinguish typo from missing collection
- Recognise a module that succeeded on the target but reported failure, and recover safely
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
Module failures cluster into five recurring shapes. Learning to tell them apart from the first three lines of the error is worth more than learning any individual module, because the wrong first guess sends you to the wrong machine and the first machine is usually where you spend the hour.
The organising question is: did this happen on the controller, or on the target? Two of the five never reach a host at all.
| # | Failure | Where |
|---|---|---|
| 1 | Module could not be resolved | Controller |
| 2 | Argument validation rejected the call | Target, before any work |
| 3 | Interpreter missing or wrong | Target, before the module started |
| 4 | Required Python library absent | Target, at module import |
| 5 | Succeeded on the target, reported failure | Target, after the work |
1. Module not found
$ ansible localhost -m ansible.builtin.stats -a "path=/tmp"localhost | FAILED! => {
"changed": false,
"msg": "Task failed: Cannot resolve 'ansible.builtin.stats' to an action or module."
}The marker is Cannot resolve. It is a controller problem, and it
happens before anything is packaged or transferred.
2. Argument validation error
$ ansible localhost -m ansible.builtin.stat -a "path=/etc/hostname folow=true"localhost | FAILED! => {
"changed": false,
"msg": "Unsupported parameters for (ansible.builtin.stat) module: folow. Supported parameters include: checksum_algorithm, follow, get_attributes, get_checksum, get_mime, get_selinux_context, path."
}Illustrative output
Three variants, all the same class:
| Message begins | Cause |
|---|---|
Unsupported parameters for (...) | Option name does not exist for this module |
missing required arguments: ... | A required option was not supplied |
argument 'x' is of type str and we were unable to convert to ... | Type mismatch |
The type variant is worth seeing, because it names every acceptable value:
$ ansible localhost -m ansible.builtin.stat -a '{"path": "/etc/hostname", "get_checksum": "banana"}'localhost | FAILED! => {
"changed": false,
"msg": "argument 'get_checksum' is of type str and we were unable to convert to bool: The value 'banana' is not a valid boolean. Valid booleans include: 0, 1, 'true', 'y', 'yes', 'on', '0', 'n', 't', 'no', '1', 'off', 'false', 'f'"
}This is technically a target-side failure — the module was transferred and rejected the arguments there — but the fault is entirely in your playbook, so treat it as a controller problem for diagnostic purposes. Nothing about the target matters.
The high-value case is when the option is templated. A variable that
rendered to the wrong type produces this error and the playbook text
looks correct. That is the situation ANSIBLE_INJECT_INVOCATION=1 was
made for: it puts the fully-resolved arguments into the result so you can
see what the module was actually called with.
3. Wrong or missing interpreter
$ ansible localhost -m ansible.builtin.ping -e ansible_python_interpreter=/usr/bin/python9localhost | FAILED! => {
"changed": false,
"module_stderr": "/bin/sh: 1: /usr/bin/python9: not found\n",
"module_stdout": "",
"msg": "The module interpreter '/usr/bin/python9' was not found.",
"rc": 127
}The markers are module_stderr containing a shell error, and rc: 127 —
the shell’s “command not found”. module_stdout is empty because the
module never produced any.
This is a managed-node problem, and it is usually one of three things: the host genuinely has no Python at the pinned path; a group variable pinned an interpreter for a host that does not match the group’s assumptions; or discovery picked something unexpected. The lesson on interpreter discovery covers the last one.
A related shape, distinguishable by the message rather than the code, is
non-empty module_stdout containing something that is not JSON. That
means a program ran and printed something Ansible could not parse —
almost always a shell profile printing a banner on login, which corrupts
the module output. MODULE FAILURE with visible motd text in
module_stdout is that, every time.
4. Missing Python library on the target
Some modules import a third-party library and fail cleanly when it is absent. The message is generated by a shared helper, so it has a consistent shape:
Failed to import the required Python library (psycopg2) on
db01.example.com's Python /usr/bin/python3. Please read the module
documentation and install it in the appropriate location. If the
required library is installed, but Ansible is using the wrong Python
interpreter, please consult the documentation on
ansible_python_interpreter
Three things in that message are doing work, and it is worth reading all of them:
- The library name,
psycopg2— what to install. - The hostname,
db01.example.com— this is the target’s view. - The interpreter path,
/usr/bin/python3— which Python was asked for the import.
That third item is the one people skip and the one that resolves the common confusion. “But I installed it” is usually true, and installed into a different Python than the one Ansible used. Compare the path in the message against where you installed the library before installing it again.
5. Succeeded on the target, reported failure
The uncomfortable one, because the play stops and the change happened anyway.
Real causes: a module completes its work and then fails while gathering the state it wants to return; a module runs a helper whose exit status does not mean what the module assumed; a connection times out after the operation committed on the target.
Recognising it: the failure message names a step that is after the change. “Failed to read back the configuration”, “unable to determine service state”, “timed out waiting for the unit to become active” — all describe verification, not action.
The recovery has two steps and the order matters:
- Verify on the target with a read-only module.
statthe file,service_factsthe unit,slurpthe config. Do not guess from the error text. - Rerun the task if it is idempotent. A purpose-built module finds
the state already correct, reports
changed: false, and either succeeds — telling you the change landed — or fails the same way, telling you it did not.
Step 2 is only available if the task was written with a module rather
than a shell line. A shell task rerun does the work twice, and whether
that is harmless depends entirely on what the command was. This is the
concrete payoff of everything the previous lessons argued: idempotency is
not tidiness, it is what makes a partial failure recoverable.
The triage order
Given an unfamiliar failure, in this order:
- Read
msgfirst. It is the module’s own summary and it is usually accurate. - Is it
Cannot resolve? Controller. Stop; do not SSH anywhere. - Is it an argument message? Your playbook. Re-run with
ANSIBLE_INJECT_INVOCATION=1if the option was templated. - Is there
module_stderror a non-zerorc? Target, and the module never ran. Look at the interpreter. - Is there
module_stdoutthat is not JSON? Target, and something polluted the stream. - Otherwise the module ran and reported a failure. Trust the message, verify the state read-only, and decide whether to rerun.
Knowledge check
Knowledge check · 4 questions
Q1. A task fails with module_stderr containing "/usr/bin/python3.13: not found" and rc 127. Which machine is the problem on, and what ran?
Q2. The "Cannot resolve X to an action or module" error looks the same whether the name is misspelled or the collection is simply not installed.
Q3. A play fails on exactly one host out of fifty with the same task. Which explanations remain plausible? Select all that apply.
Q4. A MODULE FAILURE arrives with module_stdout containing the text of the login banner. What is the actual cause?
Passing score: 75%. Answers are checked in this browser.