Skip to main content
RunBook Academy

AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust

Collections, namespaces and the FQCN

Intermediate⏱ ~18 minansibleansible-galaxyansible-doc

What you'll learn

  • Describe what a collection contains and where its parts live on disk
  • Read a fully qualified collection name and name each of its three segments
  • Predict which module runs when two collections ship the same short name
  • Explain why a repository that uses FQCN everywhere is auditable and one that does not is not

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.

Part VIII established that a bare module name is a search and a fully qualified one is an address. That lesson stopped at the boundary of ansible.builtin, because everything it needed was already installed.

This part starts where the code stops being yours. A collection is somebody else’s Python, downloaded over the internet, executed by your controller, and — for most useful collections — executed as root on every host in the play. The naming rules below are not bureaucracy. They are how you keep track of whose code you are running.

What a collection actually is

A collection is a directory tree with a manifest. Nothing more exotic than that. Here is one, freshly scaffolded, before anybody has put anything in it:

Read-only / Safethe shape of a collection
$ ansible-galaxy collection init example_corp.platform --init-path ./coll && find ./coll -maxdepth 4 | sort
- Collection example_corp.platform was created successfully
./coll
./coll/example_corp
./coll/example_corp/platform
./coll/example_corp/platform/docs
./coll/example_corp/platform/galaxy.yml
./coll/example_corp/platform/meta
./coll/example_corp/platform/meta/runtime.yml
./coll/example_corp/platform/plugins
./coll/example_corp/platform/plugins/README.md
./coll/example_corp/platform/README.md
./coll/example_corp/platform/roles

Read the path from the bottom up and the naming rule falls out of it:

  • example_corp/ — the namespace. An organisation, a vendor, a community working group. ansible, community, redhat, amazon, cisco, and — once you build one — yours.
  • platform/ — the collection name within that namespace.
  • plugins/ — the content. Subdirectories per plugin type: modules/, filter/, lookup/, inventory/, callback/, and the rest. roles/ sits alongside, because a collection can ship roles too.
  • galaxy.yml — the manifest: version, licence, dependencies, author, repository URL. This is the file you will learn to read suspiciously in lesson 5.
  • meta/runtime.yml — compatibility metadata, most usefully requires_ansible, and the redirect table that keeps old names working after a rename.

A fully qualified collection name — an FQCN — is those first two directory components plus the plugin’s own name:

example_corp . platform . to_cidr
└─ namespace ┘ └─ name ─┘ └ plugin ┘

ansible.builtin.copy follows the same rule. So does community.docker.docker_container, amazon.aws.ec2_instance and ansible.posix.mount. Three segments, always, and the first two are a directory path on your controller.

The demonstration: two collections, one short name

The rule that matters is what happens when two collections both ship a module called report. This is not hypothetical — short names like instance, network, volume, secret and user collide across cloud, container and platform collections constantly.

Two collections in a project-local collections/ directory, each with a report module that reports only which collection it came from:

Read-only / Safeexample_corp/tools/plugins/modules/report.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule

def main():
  module = AnsibleModule(argument_spec={})
  module.exit_json(changed=False, source="example_corp.tools")

if __name__ == '__main__':
  main()

other_corp/tools/plugins/modules/report.py is identical apart from the string. Now a play that lists both namespaces and calls the short name:

Read-only / Safecollide.yml
- name: Which report wins
hosts: localhost
gather_facts: false
collections:
  - example_corp.tools
  - other_corp.tools
tasks:
  - name: Short name
    report:
    register: r

  - ansible.builtin.debug:
      msg: "short name resolved to {{ r.source }}"

  - name: Fully qualified
    other_corp.tools.report:
    register: f

  - ansible.builtin.debug:
      msg: "fqcn resolved to {{ f.source }}"
Read-only / Saferun 1: example_corp listed first
$ ansible-playbook -i localhost, -c local collide.yml
TASK [Short name] **************************************************************
ok: [localhost]

TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
  "msg": "short name resolved to example_corp.tools"
}

TASK [Fully qualified] *********************************************************
ok: [localhost]

TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
  "msg": "fqcn resolved to other_corp.tools"
}

Now swap the two lines under collections: — change nothing else, not one character in any task — and run it again:

Read-only / Saferun 2: other_corp listed first
$ ansible-playbook -i localhost, -c local collide-swapped.yml
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
  "msg": "short name resolved to other_corp.tools"
}

TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
  "msg": "fqcn resolved to other_corp.tools"
}

The short-name task now runs somebody else’s module. The fully qualified task is untouched, because there was never a search to influence.

Where FQCN pays: reading a repository you did not write

You inherit a 4,000-line Ansible repository. First question, always: whose code does this run?

If the repository uses fully qualified names, that is a grep:

Read-only / Safethe dependency inventory, from a search
grep -rhoE '\b[a-z0-9_]+\.[a-z0-9_]+\.[a-z0-9_]+:' roles/ playbooks/ \
| sort -u

Every third-party collection the repository touches falls out of that, and you can compare it against requirements.yml in about a minute. A name in the code that is not in requirements.yml is an undeclared dependency; a name in requirements.yml that is not in the code is dead weight that still gets installed and still gets executed by whatever else pulls it in.

If the repository uses short names, there is no equivalent search. You have to resolve every name against the controller’s installed set, and the answer is only true for that controller on that day.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play lists two collections under collections:, both of which ship a module named volume. A colleague sorts the list alphabetically in a tidy-up commit. What is the risk?

  2. Q2. What does the namespace segment of community.docker.docker_container tell you about the collection?

  3. Q3. You inherit a repository and need to know which third-party collections it executes. Which of these help? Select all that apply.

  4. Q4. A collection that renames a module can keep existing playbooks working by adding a redirect under plugin_routing in meta/runtime.yml.

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