Ansible Collections for Reusable Modules Development
We often face situations where standard Ansible modules don't cover project needs: integration with internal APIs, custom inventory sources, or distributing automation across teams. In such cases, we develop Ansible Collections—packages that can be versioned, tested, and published independently of Ansible Core. Over the years, we have delivered more than 50 projects with custom collections, ranging from simple modules to full inventory plugins for internal CMDBs.
Collections were introduced in Ansible 2.9 as an answer to namespace and versioning chaos. Before collections, everything lived in a single repository: modules for AWS, GCP, VMware sat next to core modules and were updated together with Core. A collection is a package containing roles, plugins, modules, and filters with an independent release cycle. Today it is the de facto standard for distributing Ansible content.
Problems We Solve
- Custom modules. No standard module for your service's API? We write a module that interacts with REST API, handles errors, and supports check mode. Example: the NetBox host registration module (see below) replaces 50+ lines of bash scripts, runs 10x faster, and is more secure (token not exposed in logs).
- Inventory plugins. Is your data source (AD, OpenStack, custom CMDB) not supported by community.general? We develop a plugin that loads inventory dynamically—without wrapper scripts or workarounds.
- Content unification. Are roles, modules, and filters scattered across git repositories? We consolidate them into a collection with unified versioning and dependencies, simplifying onboarding for new engineers.
How Is an Ansible Collection Structured?
ansible-galaxy collection init mycompany.infrastructure Creates a structure:
mycompany/infrastructure/
├── galaxy.yml # metadata, version, dependencies
├── README.md
├── plugins/
│ ├── modules/ # custom modules
│ ├── inventory/ # inventory plugins
│ ├── filter/ # Jinja2 filters
│ └── lookup/ # lookup plugins
├── roles/
│ ├── hardening/
│ └── monitoring/
└── playbooks/galaxy.yml:
---
namespace: mycompany
name: infrastructure
version: 1.2.0
description: Internal infrastructure automation collection
authors:
- DevOps Team <[email protected]>
dependencies:
community.general: ">=7.0.0"
ansible.posix: ">=1.5.0"
---
Why Use Custom Modules Instead of Scripts?
A custom module is a Python script using AnsibleModule. Unlike shell or command tasks, a module:
- Supports check mode (idempotent preview);
- Returns structured data (result) for use in subsequent tasks;
- Logs sensitive parameters safely with
no_log; - Integrates with Ansible testing.
Example module for registering a host in NetBox:
# plugins/modules/netbox_register.py
from ansible.module_utils.basic import AnsibleModule
import requests
DOCUMENTATION = r'''
module: netbox_register
short_description: Register host in NetBox CMDB
options:
hostname:
description: Hostname to register
required: true
ip_prefix:
description: IP prefix to allocate from (e.g. 10.20.30.0/24)
required: true
netbox_url:
description: NetBox URL
required: true
netbox_token:
description: NetBox API token
required: true
no_log: true
'''
def run_module():
module_args = dict(
hostname=dict(type='str', required=True),
ip_prefix=dict(type='str', required=True),
netbox_url=dict(type='str', required=True),
netbox_token=dict(type='str', required=True, no_log=True),
)
result = dict(changed=False, ip_address=None)
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
headers = {'Authorization': f"Token {module.params['netbox_token']}"}
nb_url = module.params['netbox_url']
# Check if already registered
resp = requests.get(
f"{nb_url}/api/ipam/ip-addresses/",
params={'dns_name': module.params['hostname']},
headers=headers
)
if resp.json()['count'] > 0:
result['ip_address'] = resp.json()['results'][0]['address']
module.exit_json(**result)
if module.check_mode:
result['changed'] = True
module.exit_json(**result)
# Allocate IP from prefix
prefix_resp = requests.get(
f"{nb_url}/api/ipam/prefixes/",
params={'prefix': module.params['ip_prefix']},
headers=headers
)
prefix_id = prefix_resp.json()['results'][0]['id']
ip_resp = requests.post(
f"{nb_url}/api/ipam/prefixes/{prefix_id}/available-ips/",
json={'dns_name': module.params['hostname'], 'status': 'active'},
headers=headers
)
result['changed'] = True
result['ip_address'] = ip_resp.json()['address']
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
Usage in playbook:
---
- name: Register server in NetBox
mycompany.infrastructure.netbox_register:
hostname: "{{ inventory_hostname }}"
ip_prefix: "10.20.30.0/24"
netbox_url: "https://netbox.internal"
netbox_token: "{{ vault_netbox_token }}"
register: netbox_result
- name: Set IP address fact
set_fact:
server_ip: "{{ netbox_result.ip_address | ansible.utils.ipaddr('address') }}"
---
Comparison: Collection vs Role
| Criterion | Role | Collection |
|---|---|---|
| Scope | Configuration of a single service | Package of modules, plugins, roles |
| Custom modules | No | Yes, in plugins/modules |
| Inventory plugins | No | Yes |
| Filters and lookup | No | Yes |
| Versioning | Depends on repo | Independent, via galaxy.yml |
| Distribution | Git clone | Galaxy / Automation Hub |
How to Test an Ansible Collection?
Testing is a key phase. We use three levels:
- Unit tests (pytest with
ansible-test units) – verify module and filter logic on mock data. - Sanity tests (
ansible-test sanity) – validate compatibility with Ansible API, PEP8, absence of deprecated calls. - Integration tests (
ansible-test integration) – deploy test infrastructure (Docker or Vagrant) and check module operation in a real environment.
Example CI config in GitHub Actions:
---
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install ansible-core
- run: ansible-test sanity --docker -v
- run: ansible-test units --python 3.11 -v
- run: ansible-test integration --docker -v
Official Ansible testing documentation: Ansible Testing Guide
Comparison of Testing Tools
| Tool | Purpose | Speed | When to Use |
|---|---|---|---|
| ansible-test units | Unit tests for modules | Fast | Each commit |
| ansible-test sanity | Static analysis | Fast | Before PR |
| ansible-test integration | Integration tests | Slow | Before release |
| Molecule | Testing roles | Medium | For isolated roles |
Process
- Analysis – define collection composition: modules, plugins, dependencies.
- Design – develop structure, module interfaces, test scenarios.
- Development – write code for modules, plugins, roles.
- Testing – unit, sanity, integration tests in CI.
- Deployment – publish to private Galaxy or Automation Hub, documentation.
What Is Included
- Development of 2–3 custom modules (or plugins) with documentation.
- Creation of galaxy.yml with dependencies and metadata.
- CI setup (GitLab CI / GitHub Actions) with test execution on every PR.
- Publication to private Galaxy server or Automation Hub.
- Source code and README delivery.
- Team training on collection usage (1 hour).
- Compatibility guarantee with current Ansible versions (at delivery).
Estimated Timelines
- Collection with 2–3 modules and basic tests – 3–5 days.
- Inventory plugin for a specific source – 2–3 days.
- CI setup with publication – 1–2 days.
Cost is calculated individually after analyzing your requirements. Contact us – we will evaluate your project within one business day.
Common Mistakes in Collection Development
- Not including check mode – breaks idempotency.
- Storing sensitive parameters without
no_log. - Skipping sanity tests before publication (Ansible Galaxy requires passing them).
- Not versioning the collection semantically (follow SemVer).
- Not specifying dependencies in galaxy.yml – collection won't build on a clean environment.
Developing Ansible Collections is a way to standardize and accelerate automation, reducing operational costs by 30–50%. Order a custom collection for your tasks – get a ready package with tests and CI.







