Building Custom Ansible Collections: Ship Your Own Modules Without the Pain
Build custom Ansible collections that survive production.
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
- ✓Ansible 2.10+ installed. Python 3.8+ for module development. Basic Python programming knowledge.
Create a directory structure with galaxy.yml, write your Python modules in plugins/modules/, add a requirements.yml for dependencies, then build with ansible-galaxy collection build. Publish to Galaxy or use locally with collections/.
Think of a collection like a LEGO set. Instead of hunting for individual bricks (modules) all over the floor, you get a box with all the pieces you need, plus instructions (roles) and extra parts (plugins). You can share the box with your team, and everyone builds the same thing without missing pieces.
You've been there. A custom module that lives in a random Git repo, copied into every playbook with a library/ directory. Then someone updates it, breaks something, and you spend an hour tracking down which version is running where. That's the problem collections solve — they give you a single source of truth with versioning, dependencies, and a clean install path. After this article, you'll be able to build, test, and ship a custom collection that your team can consume with a single ansible-galaxy collection install command.
Why Bother? The Pain of Ad-Hoc Modules
Before collections, every team had their own hack. Some used library/ directories in playbook repos. Others symlinked a shared drive. The result: version hell. You'd fix a bug in one module, push a playbook, and three other playbooks would break because they depended on the old behavior. Collections fix this by giving you a package manager for your automation. You get semantic versioning, dependency resolution, and a single install command. No more hunting for where a module lives.
library/ in a shared playbook repo. You'll lose track of which version is deployed where. Collections are the only sane way to distribute custom modules.Anatomy of a Collection: What Goes Where
A collection is just a directory with a specific structure. The galaxy.yml manifest is your metadata — namespace, name, version, dependencies. Modules live in plugins/modules/, roles in roles/, playbooks in playbooks/. The plugins/ directory also holds action plugins, filters, and more. The key insight: module_utils is for shared Python code between your modules. Don't put utility functions in each module — that's how you get copy-paste bugs.
module_utils for any code shared between modules. It's imported as from ansible_collections.my_namespace.my_collection.plugins.module_utils.my_utils import helper_func. This keeps your modules lean and testable.Writing Your First Collection Module
Modules are Python scripts that return JSON. Ansible executes them on the target host. Your module must inherit from AnsibleModule and call module.exit_json() or module.fail_json(). The argument_spec defines your parameters. Always use supports_check_mode=True — your users will thank you. Here's a module that checks if a file exists and optionally creates it.
ANSIBLE_CHECK_MODE env var. You must check module.check_mode and return changed=True without making changes. If you forget, check mode is broken.Building and Installing Your Collection
Once your module is written, build the collection tarball with ansible-galaxy collection build. This reads galaxy.yml and packages everything. Install it locally with ansible-galaxy collection install my_namespace-my_collection-1.0.0.tar.gz. For development, use collections/ansible_collections/my_namespace/my_collection/ in your playbook repo — symlink or copy. Never install from source in production; always use the tarball.
~/.ansible/collections/. Always use ansible-galaxy collection install with the tarball. Otherwise, you'll miss the metadata and dependency resolution.Testing Your Collection: Don't Skip This
Testing a collection is non-negotiable. Use ansible-test from the ansible-core package. Write unit tests for your module logic using Python's unittest and mock the AnsibleModule calls. For integration tests, write playbooks that exercise your module and assert the output. Run ansible-test units and ansible-test integration. The classic mistake: testing only the happy path. Test failure modes — invalid params, missing files, permission errors.
ansible-test with --docker to run tests in a consistent environment. This catches OS-specific bugs before they hit production.Publishing to Ansible Galaxy
When you're ready to share, publish to Ansible Galaxy. First, create an API token on galaxy.ansible.com. Then run ansible-galaxy collection publish ./dist/my_namespace-my_collection-1.0.0.tar.gz --api-key YOUR_TOKEN. After publishing, anyone can install with ansible-galaxy collection install my_namespace.my_collection. Versioning is critical — follow semantic versioning. A breaking change? Bump the major version. A bug fix? Patch version. Never overwrite a published version — Galaxy doesn't allow it, and for good reason.
-dev or -rc suffix to Galaxy. Use a private repository or internal Galaxy server for pre-release versions. Galaxy is for stable releases only.Dependencies and Requirements
Your collection may depend on other collections or Python packages. Declare collection dependencies in galaxy.yml under dependencies. For Python dependencies, use requirements.txt in the collection root. When a user installs your collection, Ansible Galaxy will automatically install the dependent collections. Python dependencies are not auto-installed — you must document them and have users install them separately. The classic gotcha: forgetting to declare a dependency and getting a cryptic import error at runtime.
module_utils from another collection, you must add that collection as a dependency. Otherwise, the import fails with ModuleNotFoundError: No module named 'ansible_collections.other_namespace'.When NOT to Use a Collection
Collections are overkill for a single module used in one playbook. If you have one-off automation, just inline the module or use a simple script. Also, avoid collections if your team can't agree on a namespace. The namespace is global — pick once, live with it. Finally, if you need to support multiple Ansible versions, collections can be tricky. Each collection targets a specific requires_ansible version. If you need wide compatibility, test across versions.
library/ in the playbook repo — but only if you're the sole consumer. The moment a second team needs it, migrate to a collection.The Version Mismatch That Took Down Deployments
module_utils import errors across 20+ servers.my_namespace.my_collection without a version constraint, and the CI pipeline had cached an older collection tarball. The module_utils directory had been restructured in the new version, breaking imports.ansible-galaxy collection install my_namespace.my_collection:==1.2.3 to the CI pipeline. Use collections: in requirements.yml with explicit version ranges.- Always pin collection versions in your requirements file.
- Never rely on implicit latest.
ERROR! couldn't resolve module/action when running playbookansible-galaxy collection list | grep <namespace> to check if installed.
2. If not installed, run ansible-galaxy collection install <namespace>.<name>:<version>.
3. If installed but not found, check playbook's collections keyword: collections:
- <namespace>.<name>.ModuleNotFoundError: No module named 'ansible_collections...' during module executiondelegate_to or local_action).
2. Check galaxy.yml dependencies — missing dependency causes this.
3. Run ansible-galaxy collection install -r requirements.yml to install all dependencies.module.check_mode is checked before making changes.
2. Test with --check flag: ansible-playbook playbook.yml --check.
3. If module doesn't support check mode, add supports_check_mode=True in AnsibleModule constructor.ansible-galaxy collection list | grep my_namespaceansible-galaxy collection install my_namespace.my_collection:==1.0.0collections: to playbook and pin version in requirements.yml| File | Command / Code | Purpose |
|---|---|---|
| before_collections.devops | - hosts: all | Why Bother? The Pain of Ad-Hoc Modules |
| collection_structure.devops | my_collection/ | Anatomy of a Collection |
| plugins | from ansible.module_utils.basic import AnsibleModule | Writing Your First Collection Module |
| build_and_install.devops | ansible-galaxy collection build --output-path ./dist | Building and Installing Your Collection |
| tests | from unittest import TestCase | Testing Your Collection |
| publish.devops | export ANSIBLE_GALAXY_API_KEY='your_token_here' | Publishing to Ansible Galaxy |
| galaxy.yml | namespace: my_namespace | Dependencies and Requirements |
| when_not_to.devops | - hosts: localhost | When NOT to Use a Collection |
Key takeaways
Common mistakes to avoid
3 patternsNot including a galaxy.yml manifest
galaxy.yml file is required for building and publishing collections. Include namespace, name, version, and dependencies.Skipping testing before publishing
ansible-test sanity and ansible-test units before publishing. A broken collection published to Galaxy affects all users.Not versioning collections properly
meta/runtime.yml before removing them.Interview Questions on This Topic
How does Ansible resolve module names when multiple collections define the same module name?
collections keyword in the playbook to set a search order. If not specified, it falls back to the built-in modules. If two collections define the same module, the first one in the collections list wins. Always use fully qualified collection names (FQCN) like my_namespace.my_collection.my_module to avoid ambiguity.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Ansible. Mark it forged?
3 min read · try the examples if you haven't