Two roles is just two folders. Twenty roles, used by six teams across forty repos — that's a library. At that scale, conventions matter more than cleverness.
Each role lives in its own git repo. Repo name = ansible-role-<name>. The
upside is enormous: independent versioning, independent CI, independent change
ownership, and the role works as either a Galaxy import or a git-pinned dependency.
gitlab.example.com/platform/
├── ansible-role-common
├── ansible-role-mysql
├── ansible-role-proxysql
├── ansible-role-postgresql
├── ansible-role-nginx
├── ansible-role-monitoring
└── ansible-role-firewall
Tag every release with vMAJOR.MINOR.PATCH. Increment:
| Bump | Triggers | Example |
|---|---|---|
| PATCH (1.2.0 → 1.2.1) | Bug fix, no new feature, no API change | Fixed broken regex in template |
| MINOR (1.2.0 → 1.3.0) | New feature, backward-compatible | Added mysql_ssl_enabled default |
| MAJOR (1.2.0 → 2.0.0) | Breaking change to role API | Renamed mysql_pwd → mysql_root_password |
requirements.yml: version: ">=2.3.0,<3.0.0". They auto-pick up patches and features but never breaking changes.
Treat defaults/main.yml as the role's public contract. Variable names
and types in that file are the API surface. Renaming a variable is a breaking change.
Add liberally, remove never.
# v1.0 → v1.1 — adding a new optional variable is OK (MINOR)
mysql_root_password: ""
mysql_port: 3306
+ mysql_ssl_enabled: false # new variable, sensible default
# v1.x → v2.0 — renaming or removing a variable is NOT OK (MAJOR)
- mysql_pwd: "" # ← removed → BREAKING
+ mysql_root_password: ""
# To deprecate gracefully:
# - Keep the old variable working with a fallback for one MINOR version
# - Emit a deprecation warning
- name: Deprecation warning for old variable
ansible.builtin.debug:
msg: |
mysql_pwd is deprecated, use mysql_root_password instead.
mysql_pwd will be removed in v2.0.
when: mysql_pwd is defined
Every role's README documents three sections that callers care about:
- Required variables — must be set or the role fails
- Optional variables — table with name, type, default, description
- Example invocation — so a caller can copy-paste-tweak in 30 seconds
# ansible-role-mysql
Installs and configures MySQL 8 on OEL 8 / RHEL 8 / Ubuntu 22.04.
## Requirements
- Python 3 on managed nodes
- `ansible.posix` collection (firewalld module)
## Required variables
| Variable | Type | Description |
|----------|------|-------------|
| `mysql_root_password` | string | Root password (REQUIRED — supply via vault) |
## Optional variables
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `mysql_port` | int | `3306` | TCP port for mysqld |
| `mysql_max_connections` | int | `200` | Max simultaneous connections |
| `mysql_databases` | list of dict | `[]` | DBs to create — see below |
| `mysql_users` | list of dict | `[]` | Users to create — see below |
| `mysql_ssl_enabled` | bool | `false` | Enable TLS on mysqld port |
### `mysql_databases` shape
```yaml
mysql_databases:
- name: app_db
encoding: utf8mb4
collation: utf8mb4_unicode_ci
```
### `mysql_users` shape
```yaml
mysql_users:
- name: appuser
password: "{{{{ vault_appuser_pwd }}}}"
priv: "app_db.*:ALL"
host: "%"
```
## Example
```yaml
- hosts: databases
roles:
- role: company.mysql
vars:
mysql_root_password: "{{{{ vault_mysql_root_pwd }}}}"
mysql_databases:
- {{ name: myapp, encoding: utf8mb4 }}
mysql_users:
- {{ name: appuser, password: "{{ vault_app_pwd }}", priv: "myapp.*:ALL" }}
```
## License
MIT
Each role's repo has its own CI pipeline that runs molecule test against multiple
OS variants. Molecule spins up Docker or Vagrant boxes, applies the role, runs idempotency
checks, runs assertions.
---
stages:
- lint
- test
lint:
stage: lint
image: python:3.11-alpine
script:
- pip install ansible-lint yamllint
- yamllint .
- ansible-lint .
molecule_oel8:
stage: test
image: quay.io/ansible/creator-ee:latest
services:
- docker:dind
script:
- cd ansible-role-mysql
- molecule test --scenario-name oel8
molecule_ubuntu22:
stage: test
image: quay.io/ansible/creator-ee:latest
services:
- docker:dind
script:
- cd ansible-role-mysql
- molecule test --scenario-name ubuntu22
Application repos that consume the role library should pin every dependency in their
requirements.yml. The library team publishes a recommended requirements.yml
known-good combination so consumers don't have to figure out which versions of which
roles work together.
---
# requirements.yml — bundle v2025.1 (tested by the platform team)
roles:
- name: company.common
src: git+https://gitlab.example.com/platform/ansible-role-common.git
version: v2.4.1
- name: company.mysql
src: git+https://gitlab.example.com/platform/ansible-role-mysql.git
version: v3.1.0
- name: company.proxysql
src: git+https://gitlab.example.com/platform/ansible-role-proxysql.git
version: v1.4.2
- name: company.monitoring
src: git+https://gitlab.example.com/platform/ansible-role-monitoring.git
version: v0.9.5
Make it easy for any team to fix or improve a role — even if they don't own it:
- Open issue tracker, public discussion threads.
- Clear contributing guide with commit-message and tagging conventions.
- CODEOWNERS for review, but PRs from anyone welcome.
- Auto-merge for green builds + LGTM from a CODEOWNER.
You've covered roles end to end: the standard layout, defaults vs vars vs role-args precedence, Galaxy and requirements.yml, collections and FQCN, multi-role orchestration patterns, and finally the conventions that keep a 30-role library healthy across teams.
Next — Section 4: Inventory and Secrets (6 pages, 26–31). You'll learn static and dynamic inventory, custom inventory plugins, Ansible Vault for encrypted secrets, vault best practices for git workflows and CI/CD, and SSH connection tuning.