Ansible OEL 8 DevOps · OEL 8 · Roles

AnsibleReusable Role Library Design

How to structure a role library that scales across teams — naming, versioning, the public API of a role, breaking changes, plus the testing and CI conventions that keep your library healthy.

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.

PROJECT REPOS app-deploy/ requirements.yml site.yml db-deploy/ requirements.yml site.yml infra-bootstrap/ requirements.yml site.yml depend on SHARED ROLE LIBRARY (each in its own git repo) company.common SSH, NTP, sysctl company.mysql MySQL install + tuning company.proxysql ProxySQL deploy + config company.nginx Nginx + TLS company.monitoring node_exporter + Loki company.firewall firewalld baseline + per-tier Roles versioned, tagged, pinned via requirements.yml — repos stay slim

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.

BASH — repo naming convention
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
⚠ Warning: The mono-repo with all roles in one big repo seems convenient at first but causes pain later: every role's CI runs on every PR, version tags become ambiguous, blast radius of a single change covers everything. One role per repo from day one.

Tag every release with vMAJOR.MINOR.PATCH. Increment:

BumpTriggersExample
PATCH (1.2.0 → 1.2.1)Bug fix, no new feature, no API changeFixed broken regex in template
MINOR (1.2.0 → 1.3.0)New feature, backward-compatibleAdded mysql_ssl_enabled default
MAJOR (1.2.0 → 2.0.0)Breaking change to role APIRenamed mysql_pwdmysql_root_password
💡 Tip: Pin caller dependencies to MINOR ranges in 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.

YAML — versioning rules for defaults
# 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:

  1. Required variables — must be set or the role fails
  2. Optional variables — table with name, type, default, description
  3. Example invocation — so a caller can copy-paste-tweak in 30 seconds
MARKDOWN — README skeleton (steal this)
# 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.

YAML — .gitlab-ci.yml for a role repo
---
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.

YAML — known-good role bundle for v2025.1
---
# 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.
✅ Tip: End of Section 3. You now have all the tools to package automation into reusable, testable, versioned, documented roles — and to compose them into multi-tier deploys without copy-pasting code between teams.

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.