AnsibleVIII · Modules and the Module ModelThe module model
Fully qualified names and how a module is found
What you'll learn
- Explain what a short module name resolves through, and why FQCN does not
- Predict which module a name resolves to given a local library directory
- Diagnose a module that behaves differently on two controllers
- State why this course uses FQCN rather than the collections keyword
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
Both of these work:
- ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
- copy:
src: app.conf
dest: /etc/app.conf
They are not the same instruction. The first names a module. The second names a search, and the result of that search depends on what is installed on the controller running it — which is why the same playbook can behave differently on your laptop and on the CI runner with no change to the playbook.
The demonstration
Take a project with a library/ directory — the conventional place for a
custom module local to one repository — containing a file called
ping.py that returns a distinctive value:
# library/ping.py
#!/usr/bin/python
import json
print(json.dumps({"changed": False, "ping": "LOCAL OVERRIDE"}))
Now run the same module under three names:
$ ansible localhost -M ./library -m pinglocalhost | SUCCESS => {
"changed": false,
"ping": "LOCAL OVERRIDE"
}$ ansible localhost -M ./library -m ansible.builtin.pinglocalhost | SUCCESS => {
"changed": false,
"ping": "pong"
}$ ansible localhost -M ./library -m ansible.legacy.pinglocalhost | SUCCESS => {
"changed": false,
"ping": "LOCAL OVERRIDE"
}Three names, same task, two different modules. The third run is the
explanation of the first: a bare short name is
ansible.legacy.<name>, and ansible.legacy is a synthetic namespace
that searches local module paths before falling back to
ansible.builtin.
What a short name resolves through
Given a bare name like copy, Ansible resolves it in this order:
- The
collections:keyword, if the play, role or task declares one — each listed namespace is searched in order. - Local module paths — a
library/directory beside the playbook, a role’slibrary/, anything onDEFAULT_MODULE_PATH, anything passed with-M. This is theansible.legacypart. ansible.builtin— the modules shipped withansible-core.
A fully qualified name skips all of it. ansible.builtin.copy names one
specific module in one specific collection, and there is nothing for a
local file or a collections: list to intercept.
$ ansible-config dump | grep -E 'COLLECTIONS_PATHS|DEFAULT_MODULE_PATH'COLLECTIONS_PATHS(default) = ['/home/opsuser/.ansible/collections', '/usr/share/ansible/collections']
DEFAULT_MODULE_PATH(default) = ['/home/opsuser/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']Illustrative output
Note /home/opsuser/.ansible/ in both. Those are per-user directories on
the controller. Two engineers on the same shared controller, with the
same repository checked out, can resolve the same short name to different
modules — and neither of them will notice, because the playbook is
identical and the runs both succeed.
Why FQCN is the maintainable default
It is unambiguous. ansible.builtin.copy is one module. copy is
whatever the search finds today.
It survives collection installation. Installing a collection that
happens to contain a copy module cannot change the meaning of a task
that names ansible.builtin.copy. With short names, installing a
dependency can silently rewire an existing playbook.
It is greppable. grep -r 'community.general.' roles/ tells you
exactly which collections a repository depends on. Short names make that
question require a resolution pass rather than a search.
ansible-lint requires it. The fqcn rule set flags short names,
and this course adopts a lint configuration that enforces them. That is
downstream of the reasons above rather than a reason on its own.
It documents intent for the reader. ansible.posix.mount and
community.general.pam_limits tell a reviewer immediately that these are
not core modules and carry their own version and support story.
The cost is verbosity. That is the whole cost, and it is paid by a reader who was going to have to work out the namespace anyway.
The collections: keyword, and why this course does not lean on it
The collections: keyword lets a play, role or task declare namespaces
to search, so short names inside it resolve against them:
- hosts: web
collections:
- community.general
tasks:
- name: This resolves to community.general.pam_limits
pam_limits:
domain: deploy
limit_type: soft
limit_item: nofile
value: '65536'
$ ansible-doc -t keyword collectionscollections:
applies_to:
- Play
- Role
- Block
- Task
- Handler
description: "List of collection namespaces to search for modules, plugins, and
roles. ... Tasks within a role do not inherit the value of ``collections`` from
the play. To have a role search a list of collections, use the ``collections``
keyword in ``meta/main.yml`` within a role."
priority: 100
type: listIllustrative output
It works, and this course avoids it, for three reasons:
- It moves the answer away from the question. Reading
pam_limits:in a task tells you nothing; you have to scroll to the play header, and in a role you have to openmeta/main.yml. - It does not inherit into roles, as the documentation above states.
A play-level
collections:list has no effect on tasks inside a role the play includes, which is a rule people learn by being confused by it. - It reintroduces ambiguity. If two listed namespaces both provide a module of that name, order decides, and order is easy to change without noticing.
Collection trust, pinning and Galaxy are a part of their own later in the course. For now: write the full name.
Knowledge check
Knowledge check · 4 questions
Q1. A project has library/ping.py returning a distinctive value. Which of these runs the file in library/?
Q2. A play-level collections: keyword makes short names inside any role the play includes resolve against those namespaces too.
Q3. Two engineers run the same playbook from the same commit against the same inventory and get different results. Which of these are consistent with a short-name resolution problem? Select all that apply.
Q4. Which statement about ansible.builtin is accurate?
Passing score: 75%. Answers are checked in this browser.