Collection Structure¶
What You'll Learn¶
- What each directory in a collection is for
- What
galaxy.ymlandmeta/runtime.ymldeclare - How the three-part
namespace.collection.nameis built and reserved - How roles, playbooks, and plugins inside a collection are referenced
Why This Exists¶
A role packages tasks. A collection packages everything — modules, plugins, roles, playbooks, docs, and tests — under one versioned name. Knowing the layout is what lets you read someone else's collection quickly, and build one that tooling like ansible-galaxy, ansible-test, and Automation Hub accepts.
Generate One¶
The Layout¶
acme/
└── platform/
├── galaxy.yml # identity, version, dependencies
├── meta/
│ └── runtime.yml # supported ansible-core range, redirects, deprecations
├── README.md
├── CHANGELOG.md
├── plugins/
│ ├── modules/ # acme.platform.<module>
│ ├── module_utils/ # shared Python code for modules
│ ├── filter/ # Jinja2 filters: acme.platform.<filter>
│ ├── lookup/ # lookup plugins
│ ├── inventory/ # dynamic inventory plugins
│ ├── callback/ # output/notification plugins
│ └── action/ # control-node-side logic that wraps a module
├── roles/
│ └── nginx/ # acme.platform.nginx — normal role layout inside
├── playbooks/
│ └── bootstrap.yml # acme.platform.bootstrap
├── docs/
└── tests/
├── sanity/
├── unit/
└── integration/
Installed, it lives at <collections path>/ansible_collections/acme/platform/. That extra ansible_collections/ directory is required — it's how Python imports collection code.
galaxy.yml¶
namespace: acme
name: platform
version: 1.4.0
readme: README.md
authors:
- Platform Team <platform@example.com>
description: Internal modules and roles for the Acme platform
license:
- GPL-3.0-or-later
tags:
- infrastructure
- linux
dependencies:
ansible.posix: ">=1.5.0"
community.general: ">=11.0.0,<13.0.0"
repository: https://git.example.com/platform/acme.platform
build_ignore:
- .github
- "*.tar.gz"
- tests/output
| Field | Why it matters |
|---|---|
namespace + name |
The collection's identity; together they form the first two parts of every FQCN |
version |
Semantic version; a published version can never be overwritten |
dependencies |
Other collections installed automatically with this one, with version ranges |
build_ignore |
Keeps CI files and build artifacts out of the release tarball |
galaxy.yml is to a collection what meta/main.yml is to a role, but it drives dependency resolution at install time: ansible-galaxy reads it to decide which versions of which other collections to fetch.
meta/runtime.yml¶
requires_ansible: ">=2.18.0"
plugin_routing:
modules:
old_flag:
redirect: acme.platform.feature_flag
deprecation:
removal_version: 2.0.0
warning_text: Use acme.platform.feature_flag instead.
requires_ansible declares which ansible-core versions the collection supports, and Galaxy and Automation Hub require it. Raise the floor as old ansible-core releases reach end of life, so you're not promising support for versions you no longer test. plugin_routing lets you rename or move plugins without breaking existing playbooks immediately.
The Three-Part Name¶
- Namespace — an organization or person. On Galaxy, a namespace is created and owned by specific users; nobody else can publish into it. On Automation Hub, Red Hat and partners own their namespaces.
- Collection — a group of related content inside that namespace.
- Name — the module, plugin, role, or playbook.
This is why FQCNs remove ambiguity: community.general.timezone and acme.platform.timezone can both exist, and a playbook says exactly which one it means.
Using Each Kind of Content¶
- name: Use content from acme.platform
hosts: web
tasks:
- name: A module
acme.platform.feature_flag:
name: new_checkout
state: present
- name: A role inside the collection
ansible.builtin.include_role:
name: acme.platform.nginx
- name: A filter plugin
ansible.builtin.debug:
msg: "{{ groups['web'] | acme.platform.to_upstream_list(hostvars, 8080) }}"
- name: A playbook shipped in the collection
ansible.builtin.import_playbook: acme.platform.bootstrap
Roles: Standalone vs. Inside a Collection¶
The internal structure is identical (tasks/, defaults/, handlers/, templates/). What changes:
| Standalone role | Role in a collection | |
|---|---|---|
| Name | nginx or acme.nginx |
acme.platform.nginx |
| Versioning | Its own Git tag | The collection's version |
| Custom modules it needs | Separate library/ directory |
plugins/modules/ alongside it |
| Install | ansible-galaxy role install |
ansible-galaxy collection install |
Role names inside a collection can only contain lowercase letters, digits, and underscores — no dashes.
Common Mistakes¶
- Creating the directory as
acme.platform/instead ofacme/platform/, so tooling can't find it. - Omitting
meta/runtime.ymlorrequires_ansible, so publishing to Galaxy or Automation Hub fails. - Listing collection dependencies in a separate
requirements.ymlonly, instead ofgalaxy.ymldependencies, so consumers don't get them automatically. - Using dashes in role names inside a collection.
- Shipping
tests/outputor CI config in the release tarball becausebuild_ignoreis empty.
Interview Questions¶
- What's the difference between a role and a collection?
- What does
galaxy.ymldeclare, and why does it matter for dependency resolution? - What is
meta/runtime.ymlused for? - How would you rename a module in a published collection without breaking existing playbooks?
Next¶
Continue to Installing and Using Collections.