Skip to main content
RunBook Academy

AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust

Shipping your own internal collection

Advanced⏱ ~22 minansible-galaxy

What you'll learn

  • Decide whether a body of automation should become a versioned collection
  • Fill in galaxy.yml with the fields that matter to a consumer
  • Build and install an internal collection, and read its version honestly
  • Apply semantic versioning to automation, including what counts as a breaking change

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.

Everything in this part so far has been about consuming other people’s automation carefully. This lesson turns it around: at some point your own roles become other people’s dependency, and every trust question you have been asking gets asked about you.

When it is worth it

A collection is a versioned, packaged, distributable boundary. Boundaries cost something. Do not build one until you have the problem it solves.

Build a collection when:

  • Two or more repositories need the same automation, and the current answer is a git submodule, a roles/ directory copied between them, or a requirements.yml pointing at a git branch. Those all resolve to “whatever is on that branch today”, which is lesson 4’s problem in a different costume.
  • Consumers are not the authors. A platform team publishing roles for application teams needs a version number to have a conversation about, because “we changed it, re-run it” does not scale past the people you can walk over to.
  • You need to ship plugins, not just roles. A library/ directory and a filter_plugins/ directory at the top of a playbook repository work for one repository. A filter that three repositories need has to be in a collection; that is the only distribution mechanism for plugins.
  • You are vendoring a third-party fix. Lesson 5’s option 2: you need a patched module from a collection you cannot get a release out of. Putting it in your own collection gives the patch a version and a diff instead of a local edit that lesson 6’s verify will flag.

Do not build one when: you have one repository, one team, and roles that are only ever used by the playbooks sitting next to them. A roles/ directory is fine, and packaging it adds a build-and-publish step to every change for no consumer’s benefit.

Scaffolding

Configuration changecreate the skeleton
$ ansible-galaxy collection init example_corp.platform --init-path ./collections_src
- Collection example_corp.platform was created successfully
Read-only / Safewhat it made
$ find ./collections_src -maxdepth 4 | sort
./collections_src
./collections_src/example_corp
./collections_src/example_corp/platform
./collections_src/example_corp/platform/docs
./collections_src/example_corp/platform/galaxy.yml
./collections_src/example_corp/platform/meta
./collections_src/example_corp/platform/meta/runtime.yml
./collections_src/example_corp/platform/plugins
./collections_src/example_corp/platform/plugins/README.md
./collections_src/example_corp/platform/README.md
./collections_src/example_corp/platform/roles

Your roles go under roles/, one directory each, in exactly the layout they already have. Your plugins go under plugins/<type>/filter/, lookup/, modules/, inventory/, and so on. Nothing about a role changes when it moves into a collection except how it is addressed: example_corp.platform.baseline rather than baseline.

galaxy.yml, and the fields a consumer reads

The generated file is heavily commented. The fields that matter to somebody running lesson 5’s review against you:

Read-only / Safegalaxy.yml
namespace: example_corp
name: platform
version: 1.0.0
readme: README.md
authors:
- Platform Engineering <platform@example.com>

description: Baseline host configuration and estate-wide primitives.
license:
- GPL-3.0-or-later
tags:
- infrastructure

# Collections this one requires, as namespace.name: version-range.
dependencies:
ansible.posix: '>=3.0.0,<4.0.0'

repository: https://git.example.com/platform/ansible-platform
documentation: https://docs.example.com/platform/ansible
homepage: https://docs.example.com/platform
issues: https://git.example.com/platform/ansible-platform/-/issues

build_ignore:
- .gitlab-ci.yml
- tests/integration/inventory

Four notes on filling it in.

namespace and name must be valid Python identifiers. Lowercase alphanumeric and underscores, no hyphens, no leading digit, no consecutive underscores — because they become package path components. example_corp, not example-corp.

repository and issues are the two fields that make you reviewable. A consumer following lesson 5’s checklist reads these from MANIFEST.json after installing. If they point nowhere, you have published something that fails your own intake review.

dependencies is a range, and you are now the collection whose transitive dependency somebody else has to pin. Be conservative: '>=3.0.0,<4.0.0' says what you tested and what you believe is compatible. '*' says you did not think about it.

build_ignore keeps CI configuration and test fixtures out of the artefact. galaxy.yml, *.pyc, *.retry and .git are always filtered; everything else you ship, including anything you left in the directory.

Build and install

Configuration changebuild the artefact
$ ansible-galaxy collection build --output-path ../../../dist
Created collection for example_corp.platform at /home/opsuser/project/dist/example_corp-platform-1.0.0.tar.gz

The filename encodes namespace, name and version, which is what makes a directory of these a usable local index.

Configuration changeinstall it like any other collection
$ ansible-galaxy collection install ./dist/example_corp-platform-1.0.0.tar.gz -p ./collections --offline
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'example_corp.platform:1.0.0' to '/home/opsuser/project/collections/ansible_collections/example_corp/platform'
example_corp.platform:1.0.0 was installed successfully
Read-only / Safeit has a real version now
$ ansible-galaxy collection list -p ./collections
# /home/opsuser/project/collections/ansible_collections
Collection            Version
--------------------- -------
example_corp.platform 1.0.0

Compare that with the same collection referenced as a source tree, which is what a git clone into the collections directory gives you:

Read-only / Safethe same content, no version
$ 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 *

That * is the entire argument for doing this properly. Both directories contain the same automation. Only one of them can be pinned, verified, rolled back or reported in an audit.

Publishing to an internal server is one more command:

Configuration changepublish to the internal hub
ansible-galaxy collection publish ./dist/example_corp-platform-1.0.0.tar.gz \
--server internal_hub

--server names an entry from the server_list configured in lesson 7. The token comes from the environment, not from ansible.cfg.

Semantic versioning, applied to automation

Galaxy requires semantic versions and the resolver relies on them. The mapping to automation is not always obvious, so state it explicitly in your README.md:

ChangeBumpWhy
New role, or a new optional variable with a defaultMinorExisting consumers unaffected
Bug fix that makes a task correctly idempotentPatchUnless somebody depended on the wrong behaviour
New required variable with no defaultMajorEvery consumer’s play now fails until they set it
Renaming a variable, role or pluginMajorUnless you ship a redirect and deprecate first
Changing a default valueMajorThis is the one people get wrong
Dropping support for an OS the roles handledMajorConsumers on it are broken
Raising requires_ansibleMajorConsumers on older core cannot install

Knowledge check

Knowledge check · 4 questions

  1. Q1. An internal role changes the default of a variable from 2 to auto. No consumer edited a playbook. How should the collection be versioned?

  2. Q2. Which situations justify packaging existing roles as a collection? Select all that apply.

  3. Q3. ansible-galaxy collection build packages the current git commit, so an artefact built from a tagged checkout cannot contain uncommitted files.

  4. Q4. A collection appears in ansible-galaxy collection list with a version of * on a production controller. What does that indicate about how it got there?

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