Skip to main content
RunBook Academy

AnsibleVIII · Modules and the Module ModelThe module model

Fully qualified names and how a module is found

Intermediate⏱ ~17 minansibleansible-galaxy

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

Not yet marked complete on this device.

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:

Read-only / Safethe short name
$ ansible localhost -M ./library -m ping
localhost | SUCCESS => {
  "changed": false,
  "ping": "LOCAL OVERRIDE"
}
Read-only / Safethe fully qualified name
$ ansible localhost -M ./library -m ansible.builtin.ping
localhost | SUCCESS => {
  "changed": false,
  "ping": "pong"
}
Read-only / Safethe name the short form actually means
$ ansible localhost -M ./library -m ansible.legacy.ping
localhost | 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:

  1. The collections: keyword, if the play, role or task declares one — each listed namespace is searched in order.
  2. Local module paths — a library/ directory beside the playbook, a role’s library/, anything on DEFAULT_MODULE_PATH, anything passed with -M. This is the ansible.legacy part.
  3. ansible.builtin — the modules shipped with ansible-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.

Read-only / Safewhere collections are searched for
$ 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'
Read-only / Safethe keyword documents itself
$ ansible-doc -t keyword collections
collections:
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: list

Illustrative output

It works, and this course avoids it, for three reasons:

  1. 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 open meta/main.yml.
  2. 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.
  3. 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

  1. Q1. A project has library/ping.py returning a distinctive value. Which of these runs the file in library/?

  2. Q2. A play-level collections: keyword makes short names inside any role the play includes resolve against those namespaces too.

  3. 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.

  4. Q4. Which statement about ansible.builtin is accurate?

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