Skip to main content
RunBook Academy

AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust

Proving which artefact you installed

Intermediate⏱ ~17 minansibleansible-galaxyansible-configpip

What you'll learn

  • Establish a complete controller manifest with four read-only commands
  • Explain why ansible --version answers only a fraction of the question
  • Diagnose "it works on my controller" from a manifest diff rather than by guessing
  • State what ansible-core alone can and cannot do, from evidence rather than reputation

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 III argued which artefact belongs on a production controller — ansible-core with pinned collections rather than the batched community package. That argument is settled. This lesson is about the operational sequel: on the machine in front of you right now, which one is installed, and how would you prove it to somebody who disagrees?

The question comes up in exactly one situation, and it comes up constantly: a playbook works for one engineer and fails for another, from the same commit, against the same inventory. The instinct is to compare ansible --version. That comparison is close to useless on its own, and this lesson is mostly about why.

What ansible --version does and does not tell you

Read-only / Safethe version banner
$ ansible --version
ansible [core 2.21.3]
config file = None
configured module search path = ['/home/opsuser/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /opt/ansible/venv/lib/python3.12/site-packages/ansible
ansible collection location = /home/opsuser/.ansible/collections:/usr/share/ansible/collections
executable location = /opt/ansible/venv/bin/ansible
python version = 3.12.7 [GCC 13.2.0] (/opt/ansible/venv/bin/python3)
jinja version = 3.1.6
pyyaml version = 6.0.3 (with libyaml v0.2.5)

Illustrative output

It answers four questions honestly:

  • Which ansible-core. core 2.21.3 — the engine version.
  • Which Python and which install. The executable location and python version lines together identify the virtualenv or pipx environment. If those two disagree with what you expected, stop here; you are on a different controller than you thought.
  • Which config file. config file = None means no ansible.cfg was found — which is itself a finding, because a project that expects one has just been run from the wrong directory.
  • Where collections would be looked for.

And here is what it does not tell you: which collections are actually there. That is the whole gap. core 2.21.3 on two machines is entirely compatible with one of them having community.docker and the other not, or with the two having versions two years apart.

The four commands that make a manifest

A controller manifest is four read-only commands. Run them on both machines, diff the output, and the difference is in front of you.

Read-only / Safecontroller manifest
ansible --version
ansible-galaxy collection list
ansible-config dump --only-changed
pip list 2>/dev/null | grep -Ei 'ansible|jinja|resolvelib'

Take them in turn.

ansible-galaxy collection list

This is the command the version banner is missing. On a bare ansible-core install it prints nothing at all:

Read-only / Safea bare ansible-core controller
$ ansible-galaxy collection list

No output. No error, no headers, exit status 0. That is what ansible-core on its own looks like, and it is the single most useful piece of evidence in this diagnosis: it means every module in the repository must be ansible.builtin or the run will fail.

Once something is installed, the output names the path it was found under, which matters as much as the versions:

Read-only / Safeafter installing one collection with dependencies
$ ansible-galaxy collection list -p ./gx
# /home/opsuser/project/gx/ansible_collections
Collection                               Version
---------------------------------------- -------
community.docker                         4.7.0
community.library_inventory_filtering_v1 1.1.5

Two collections from one request. The second is a transitive dependency, and it is the subject of lesson 4.

There is a machine-readable form, which is what you want in CI:

Read-only / Safethe form you can diff
$ ansible-galaxy collection list -p ./gx --format json
{"/home/opsuser/project/gx/ansible_collections": {"community.docker": {"version": "4.7.0"}, "community.library_inventory_filtering_v1": {"version": "1.1.5"}}}

--format yaml is also accepted. Either beats parsing the human table, which is aligned with spaces and will change.

ansible-config dump --only-changed

Part VI covered this command in full. Its role in the manifest is narrow and important: it reduces a couple of hundred settings to the handful this controller has altered, including the ones set by environment variables that no file records. A controller with ANSIBLE_COLLECTIONS_PATH exported in one engineer’s shell profile is a different controller, and this is the command that shows it.

pip list

The engine’s own dependencies. Most of the time this is noise; twice a year it is the answer, when a Jinja or resolvelib version explains a templating or dependency-resolution difference the other three commands cannot.

Read-only / Safethe engine's dependencies
$ pip list | grep -Ei 'ansible|jinja|resolvelib|pyyaml'
ansible-compat            26.6.0
ansible-core              2.21.3
ansible-lint              26.6.0
Jinja2                    3.1.6
PyYAML                    6.0.3
resolvelib                1.2.1

Note what is absent: there is no package called ansible in that list. That absence is the proof that this controller has ansible-core rather than the community package — the community package installs a distribution literally named ansible, and it carries hundreds of collections with it.

What ansible-core alone actually gives you

Not an opinion — a count:

Read-only / Safethe modules you have with nothing installed
$ ansible-doc -t module -l ansible.builtin | wc -l
71

Seventy-one modules. That covers files, templates, packages via the package/apt/dnf family, services, users, groups, cron, systemd, mounts via ansible.posix… no. mount is not in that seventy-one; it is in ansible.posix, which is a collection.

That last correction is the shape of every surprise in this area. The modules people assume are “core” and are not include ansible.posix.mount, ansible.posix.sysctl, ansible.posix.firewalld, community.general.timezone and community.general.ufw. They feel core because they are ubiquitous in examples written against the community package.

Knowledge check

Knowledge check · 4 questions

  1. Q1. ansible-galaxy collection list prints nothing at all and exits 0. What have you learned?

  2. Q2. A playbook works for engineer A and fails for engineer B with "Cannot resolve" on a module name. Both report core 2.21.3. Which findings would explain it? Select all that apply.

  3. Q3. Which command output distinguishes an ansible-core install from the community ansible package most directly?

  4. Q4. ansible.builtin.mount is available on any controller running ansible-core 2.21.

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