AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust
Where collections live and which copy wins
What you'll learn
- Name the four locations a collection can be loaded from and the order they are searched
- Prove which copy of a collection a given run will load
- Explain why a playbook-adjacent collections directory is invisible to ansible-doc
- Recognise a source-tree collection from its version and say why it cannot be pinned
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
A collection is a directory. Ansible looks for it in more than one
place, takes the first one it finds, and does not mention the others.
That is the entire mechanism, and it produces one of the most
disorienting failures in the tool: a playbook that works and an
ansible-doc that says the collection does not exist.
Both are correct. They search different paths.
The four places
$ ansible-config dump | grep COLLECTIONS_PATHSCOLLECTIONS_PATHS(default) = ['/home/opsuser/.ansible/collections', '/usr/share/ansible/collections']Illustrative output
Two paths there, plus two more that do not appear in that output at all:
1. A playbook-adjacent collections/ directory. If the directory
containing the playbook has a collections/ansible_collections/...
tree, it is searched — and searched first. This is the project-local
install and it is the one this course recommends, because it puts the
dependency inside the repository where review can see it.
2. COLLECTIONS_PATHS, in order. By default a per-user directory
followed by a system-wide one. ANSIBLE_COLLECTIONS_PATH or
collections_path in ansible.cfg replaces the list.
3. Inside a role, at <role>/collections/, for a role that vendors
its own dependencies.
4. Adjacent to another collection, for a collection that ships
inside another collection’s tree. Rare; mentioned so that you recognise
it when find turns one up.
First match wins. There is no merge, no version comparison, no warning that a second copy exists.
The demonstration: loaded by the playbook, invisible to ansible-doc
A project with a collection in the playbook-adjacent directory —
collections/ansible_collections/example_corp/platform/ — containing
one filter plugin. Run a playbook that uses it:
$ ansible-playbook -i localhost, filter-test.ymlTASK [Use the collection filter] ***********************************************
ok: [localhost] => {
"msg": 24
}It works. The filter resolved, ran, and returned. Now ask ansible-doc
about the same plugin, in the same directory, in the same shell:
$ ansible-doc -t filter example_corp.platform.to_cidr[ERROR]: filter example_corp.platform.to_cidr Missing documentation (or could not parse documentation): 'Invalid plugin FQCN (example_corp.platform.to_cidr): unable to locate collection example_corp.platform': unable to locate collection example_corp.platform: No module named 'ansible_collections.example_corp'Nothing is broken. ansible-playbook searches the playbook-adjacent
directory because it knows where the playbook is. ansible-doc has no
playbook, so it searches COLLECTIONS_PATHS only — and the collection
is not there.
Point the environment variable at the project and it appears:
$ ANSIBLE_COLLECTIONS_PATH=./collections ansible-doc -t filter example_corp.platform.to_cidr> FILTER example_corp.platform.to_cidr (/home/opsuser/project/collections/ansible_collections/example_corp/platform/plugins/filter/netmask.py)
Returns the CIDR prefix length for a dotted-quad IPv4 netmask.
OPTIONS (red indicates it is required):
_input A dotted-quad netmask such as 255.255.255.0.
type: strThat header line is the answer to “which copy”. ansible-doc prints
the absolute path of the file it loaded. When two controllers disagree
about a module’s behaviour, this is the comparison that ends the
argument, and it is one command.
Proving which copy, in three commands
ansible-config dump | grep COLLECTIONS_PATHS
ansible-galaxy collection list
ansible-doc -t module community.docker.docker_container | head -1The first says where it will look. The second says what is there, with the path each set was found under printed as a comment header. The third says which file was actually opened for one specific plugin.
Run all three when the question is “why does this behave differently here”, and run the third one on both machines when the answer is not obvious from the first two.
$ ansible-galaxy collection list# /home/opsuser/.ansible/collections/ansible_collections
Collection Version
----------------- -------
community.general 12.4.0
# /usr/share/ansible/collections/ansible_collections
Collection Version
----------------- -------
community.general 9.5.1
ansible.posix 3.0.0Illustrative output
Two copies of community.general, at versions three major releases
apart. The per-user one is earlier in the path, so it wins. Nothing
about a run will tell you the system copy exists — only this command
does.
The version that is not a version
Install a collection from a Galaxy tarball and it carries a
MANIFEST.json with its version in it. Clone a collection from git
into your collections directory and it does not:
$ ansible-galaxy collection list -p ./collections[WARNING]: Collection at '/home/opsuser/project/collections/ansible_collections/example_corp/platform' does not have a MANIFEST.json file, nor has it galaxy.yml: cannot detect version.
# /home/opsuser/project/collections/ansible_collections
Collection Version
--------------------- -------
example_corp.platform ** is not a version. It means this is a working tree and I have no
idea what is in it.
A * in a production controller’s manifest is a finding. It means the
content came from somewhere other than a published artefact —
a git clone, an rsync, a manual copy — so there is no checksum to
verify, no version to pin, and no way to reproduce this controller
except by knowing which commit somebody happened to have checked out.
Compare with the same collection installed from a built artefact:
$ ansible-galaxy collection list -p ./collections# /home/opsuser/project/collections/ansible_collections
Collection Version
--------------------- -------
example_corp.platform 1.0.0Knowledge check
Knowledge check · 4 questions
Q1. ansible-playbook runs a task using example_corp.platform.to_cidr successfully, but ansible-doc -t filter example_corp.platform.to_cidr reports that it cannot locate the collection. What is happening?
Q2. ansible-galaxy collection list shows community.general 12.4.0 under ~/.ansible/collections and community.general 9.5.1 under /usr/share/ansible/collections. Which one does a run load?
Q3. A collection listed with a version of * came from a source checkout or a manual copy rather than a built artefact.
Q4. Which commands contribute evidence about which copy of a collection a run will load? Select all that apply.
Passing score: 75%. Answers are checked in this browser.