<aside> 🧭
Module 06 · Collections & Content Structure
Packaging, versioning and distributing Ansible content — and laying out a repository a team can actually work in. This is the module that turns "my playbooks" into "our platform".
🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)
Prerequisite: Modules 01–05. You met namespaces, collections and Galaxy in Module 01 Part A4 — this module goes from consuming them to building them.
</aside>
<aside> 📖
Official docs: Using collections · Developing collections · Collection structure
</aside>
<aside> 📱
The analogy. Think of an app in a phone's app store. It has a publisher and a name, and the store always shows you both — because there are four apps called "Notes" and only one of them is the one you meant. It has a version number, so "it worked on 3.1" is a precise statement rather than a feeling. It ships everything it needs in one download. And it can say "requires iOS 15 or later".
A collection is that app: amazon is the publisher, aws is the name, and Galaxy is the store. Each of those four everyday properties maps onto a real file you are about to meet — the publisher and name live in galaxy.yml, and "requires iOS 15 or later" is requires_ansible in meta/runtime.yml.
</aside>
A role packages one concern. A collection packages everything a domain needs — including the things a role structurally cannot hold: modules, plugins and playbooks.
mycompany/platform/
galaxy.yml <- the manifest: namespace, name, version, dependencies
README.md <- what this collection is for
LICENSE
meta/
runtime.yml <- requires_ansible, action_groups, plugin redirects
plugins/
modules/ -> mycompany.platform.create_tenant
module_utils/ <- shared Python for those modules
action/ <- action plugins (run on the control node)
filter/ -> mycompany.platform.to_subnet
lookup/ -> lookup('mycompany.platform.vault_secret', ...)
inventory/ -> plugin: mycompany.platform.cmdb
callback/ <- custom output formatting
connection/ <- custom transports
test/ <- custom Jinja2 tests
roles/
webserver/ -> mycompany.platform.webserver
monitoring/
playbooks/
site.yml -> mycompany.platform.site
files/ templates/ tasks/
docs/
changelogs/
changelog.yaml
tests/
sanity/ unit/ integration/
<aside> 🔑
Every directory maps directly to an FQCN. A module in plugins/modules/create_tenant.py becomes mycompany.platform.create_tenant. A role in roles/webserver/ becomes mycompany.platform.webserver. A filter in plugins/filter/ becomes mycompany.platform.to_subnet.
That is the whole naming system from Module 01 Part A4, seen from the producing side rather than the consuming side.
</aside>
<aside> 💡
Only galaxy.yml is mandatory. A collection containing nothing but roles/ is perfectly valid, and that is exactly how most internal collections start — as a way to version and distribute a handful of roles as one unit. Add plugins later when you actually need them.
</aside>
galaxy.yml — the manifest<aside> 🔢
The analogy. Think about what an app's version number actually promises you. A small last-number update is a bug fix — you install it without a thought. The middle number going up means new features arrived, but everything you already do still works. A major version change is the one where the whole interface gets redesigned and the button you pressed every morning has moved.
That is semantic versioning, and it is a promise rather than a number. Shipping a redesign as a minor update is how an app loses its users' trust, and it is exactly how a collection loses its consumers' — because their pinned >=2.1,<3.0 was them believing you.
</aside>
---
namespace: mycompany
name: platform
version: 1.4.2 # MUST be semantic versioning
readme: README.md
authors:
- Zaeem Mazhar <[email protected]>
description: Internal platform automation - web, monitoring and base configuration
license_file: LICENSE
tags:
- infrastructure
- linux
dependencies:
ansible.posix: ">=1.5.0,<2.0.0"
community.general: ">=8.0.0"
repository: <https://git.internal/ansible/platform>
documentation: <https://docs.internal/ansible/platform>
issues: <https://git.internal/ansible/platform/-/issues>
build_ignore: # excluded from the built artifact
- .git
- .gitlab-ci.yml
- molecule
- "*.tar.gz"
<aside> ⚠️
Semantic versioning is enforced, not suggested. Galaxy rejects a version that is not MAJOR.MINOR.PATCH. That matters because consumers pin ranges like ">=1.4.0,<2.0.0" — which only means anything if you honour the contract:
MAJOR — a breaking change: a renamed variable, a removed role, changed default behaviour.
MINOR — new functionality, backwards compatible.
PATCH — a bug fix, no interface change.
Bumping a MINOR for something that actually breaks consumers is how you lose a team's trust in your collection.
</aside>
meta/runtime.yml---
requires_ansible: ">=2.15.0" # fails clearly on an older control node
action_groups: # lets consumers apply module_defaults in bulk
platform:
- create_tenant
- delete_tenant
plugin_routing: # keeps old names working after a rename
modules:
old_module_name:
redirect: mycompany.platform.new_module_name
removed_module:
tombstone:
removal_version: "2.0.0"
warning_text: "Use new_module_name instead"
<aside> 🎯
plugin_routing is how you rename something without breaking every consumer. A redirect keeps the old FQCN working silently; a tombstone fails with a message telling people what to use instead.
This is the mechanism the Ansible project itself used during the 2.10 split — which is why bare yum: still resolves today. Knowing that connection is a genuinely good answer to "how do collections handle deprecation?"
</aside>
🧪 Exercise A3.1 — Scaffold a collection and read what you get