Introduction
Ansible Galaxy is the community hub for sharing Ansible roles and collections. This guide covers creating your own roles and collections from scratch, testing them with Molecule and ansible-lint, and publishing to Galaxy (public) or Private Automation Hub (enterprise). Whether you're packaging internal automation for your team or contributing to the community, this is the complete workflow.
Roles vs Collections
| Feature | Role | Collection |
|---|---|---|
| Contains | Tasks, handlers, templates, vars | Roles + modules + plugins + docs |
| Namespace | username.role_name | namespace.collection_name |
| Install | ansible-galaxy role install | ansible-galaxy collection install |
| Versioning | Git tags | galaxy.yml version field |
| Dependencies | meta/main.yml | galaxy.yml + requirements.yml |
| Publish to | Galaxy (role) | Galaxy (collection) or Automation Hub |
Use collections for anything with custom modules, plugins, or multiple roles. Use standalone roles for simple, single-purpose automation.
Create a Role
Scaffold
ansible-galaxy role init my_nginx
cd my_nginx
Structure
my_nginx/
├── defaults/
│ └── main.yml # Default variables (lowest priority)
├── files/ # Static files to copy
├── handlers/
│ └── main.yml # Handlers (notify triggers)
├── meta/
│ └── main.yml # Role metadata + dependencies
├── molecule/
│ └── default/ # Molecule test scenario
├── tasks/
│ └── main.yml # Main task list
├── templates/ # Jinja2 templates
├── tests/
│ ├── inventory
│ └── test.yml
├── vars/
│ └── main.yml # Role variables (high priority)
└── README.md
defaults/main.yml
---
nginx_port: 80
nginx_server_name: _
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_client_max_body_size: 64m
nginx_sites: []
tasks/main.yml
---
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
validate: "nginx -t -c %s"
notify: restart nginx
- name: Deploy site configs
ansible.builtin.template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
mode: '0644'
loop: "{{ nginx_sites }}"
notify: reload nginx
- name: Enable sites
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ item.name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ nginx_sites }}"
notify: reload nginx
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers/main.yml
---
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
meta/main.yml
---
galaxy_info:
author: Luca Berton
description: Install and configure nginx web server
company: AnsibleByExample
license: MIT
min_ansible_version: "2.15"
platforms:
- name: Ubuntu
versions:
- noble
- jammy
- name: EL
versions:
- "9"
galaxy_tags:
- nginx
- web
- webserver
dependencies: []
README.md
# my_nginx
Install and configure nginx.
## Requirements
None.
## Role Variables
| Variable | Default | Description |
|---|---|---|
| `nginx_port` | `80` | Listen port |
| `nginx_server_name` | `_` | Default server name |
| `nginx_worker_processes` | `auto` | Worker processes |
| `nginx_sites` | `[]` | List of site configs |
## Example Playbook
- hosts: webservers
roles:
- role: my_nginx
nginx_port: 8080
nginx_sites:
- name: myapp
domain: app.example.com
upstream: 127.0.0.1:3000
## License
MIT
Create a Collection
Scaffold
ansible-galaxy collection init my_namespace.my_collection
cd my_namespace/my_collection
Structure
my_namespace/my_collection/
├── galaxy.yml # Collection metadata
├── README.md
├── docs/
├── meta/
│ └── runtime.yml # Plugin routing
├── plugins/
│ ├── modules/ # Custom modules
│ │ └── my_module.py
│ ├── inventory/ # Inventory plugins
│ ├── filter/ # Filter plugins
│ ├── lookup/ # Lookup plugins
│ └── callback/ # Callback plugins
├── roles/
│ └── webserver/ # Bundled roles
│ ├── tasks/
│ └── defaults/
├── playbooks/ # Bundled playbooks
├── tests/
└── changelogs/
└── changelog.yaml
galaxy.yml
---
namespace: my_namespace
name: my_collection
version: 1.0.0
readme: README.md
authors:
- Luca Berton <luca@lucaberton.it>
description: My collection of Ansible roles and plugins
license:
- MIT
license_file: LICENSE
tags:
- infrastructure
- automation
- linux
repository: https://github.com/myorg/my_collection
documentation: https://github.com/myorg/my_collection/blob/main/README.md
homepage: https://ansiblebyexample.com
issues: https://github.com/myorg/my_collection/issues
# Dependencies
dependencies:
community.general: ">=8.0.0"
ansible.posix: ">=1.5.0"
# Build ignore
build_ignore:
- .git
- .github
- tests/output
- "*.tar.gz"
Custom Module
# plugins/modules/my_module.py
#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: my_module
short_description: Example custom module
description:
- This is an example custom module for the collection.
options:
name:
description: The name parameter.
required: true
type: str
state:
description: Desired state.
choices: ['present', 'absent']
default: present
type: str
author:
- Luca Berton (@lucab85)
'''
EXAMPLES = r'''
- name: Example usage
my_namespace.my_collection.my_module:
name: test
state: present
'''
RETURN = r'''
message:
description: Result message.
returned: always
type: str
'''
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
),
supports_check_mode=True,
)
name = module.params['name']
state = module.params['state']
result = dict(changed=False, message=f'{name} is {state}')
if module.check_mode:
module.exit_json(**result)
# Your logic here
result['changed'] = True
module.exit_json(**result)
if __name__ == '__main__':
main()
Test Before Publishing
# Lint
ansible-lint roles/ plugins/
# Molecule (for roles)
cd roles/webserver
molecule test
# Sanity tests (for collections)
ansible-test sanity --docker
# Unit tests
ansible-test units --docker
# Integration tests
ansible-test integration --docker
Build and Publish
Build Collection
cd my_namespace/my_collection
ansible-galaxy collection build
# Creates my_namespace-my_collection-1.0.0.tar.gz
Publish to Galaxy
# Get API key from https://galaxy.ansible.com/me/preferences
ansible-galaxy collection publish \
my_namespace-my_collection-1.0.0.tar.gz \
--api-key YOUR_API_KEY
Publish to Private Automation Hub
ansible-galaxy collection publish \
my_namespace-my_collection-1.0.0.tar.gz \
--server https://hub.example.com/api/galaxy/content/inbound-collections/ \
--api-key YOUR_PAH_TOKEN
Publish Role to Galaxy
# Role must be on GitHub
# Import via Galaxy web UI or:
ansible-galaxy role import github_username repo_name
Version Management
# Bump version in galaxy.yml
# Build
ansible-galaxy collection build
# Tag in git
git tag v1.1.0
git push --tags
# Publish
ansible-galaxy collection publish my_namespace-my_collection-1.1.0.tar.gz --api-key KEY
CI/CD Pipeline
# .github/workflows/release.yml
name: Release Collection
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip install ansible-core ansible-lint
- name: Lint
run: ansible-lint
- name: Build
run: ansible-galaxy collection build
- name: Publish to Galaxy
run: |
ansible-galaxy collection publish \
*.tar.gz \
--api-key ${{ secrets.GALAXY_API_KEY }}
Best Practices
- Semantic versioning — major.minor.patch in
galaxy.yml - Test everything — Molecule for roles, ansible-test for collections
- Document variables — every default should be in README with description
- Use FQCN —
ansible.builtin.copynotcopyin all tasks - Pin dependencies — specify minimum versions in galaxy.yml
- Changelog — maintain
changelogs/changelog.yaml - Validate before publish —
ansible-lint+ansible-test sanity - README with examples — users should be able to copy-paste and run
Related Articles
Conclusion
Creating and publishing Ansible roles and collections follows a clear workflow: scaffold with ansible-galaxy init, write tasks/modules, test with Molecule and ansible-lint, build with ansible-galaxy collection build, and publish with ansible-galaxy collection publish. For internal teams, publish to Private Automation Hub; for the community, publish to Galaxy. Every reusable piece of automation should be packaged as a role (simple) or collection (modules + roles + plugins).