Ansible OEL 8 DevOps · OEL 8 · Roles

AnsibleRoles — Structure, Dependencies, Search Path

The standard role layout, what each subdirectory does, how roles depend on other roles, and the four places Ansible looks for a role when you reference one by name.

A playbook with 200 inline tasks works — until you want to deploy MySQL on a second project, or share the install logic with another team. A role packages a self-contained chunk of automation (tasks + handlers + templates + defaults + documentation) into a directory you can drop into any playbook with one line.

💡 Tip: If you find yourself copy-pasting tasks between two playbooks, that's the moment to extract a role. Roles are reusable units of automation, the way functions are reusable units of code.

Every role follows the same skeleton — Ansible auto-discovers the right files because of the directory names. You don't need to wire anything together manually.

roles/mysql/ one role ├── tasks/ │ └── main.yml — task list (the work) ├── handlers/ │ └── main.yml — handlers callable from this role ├── defaults/ │ └── main.yml — LOWEST precedence vars (overridable) ├── vars/ │ └── main.yml — role-internal "constants" ├── templates/ │ └── my.cnf.j2 — Jinja2 templates (rendered to host) ├── files/ │ └── repo.gpg — static files (copied as-is) ├── meta/main.yml — role metadata + dependencies └── README.md — what the role does, vars it accepts
DirectoryAuto-loaded?Purpose
tasks/main.ymlYes — entry pointThe list of tasks the role runs
handlers/main.ymlYesHandlers callable from this role's tasks
defaults/main.ymlYesLOWEST-precedence variables
vars/main.ymlYesRole-internal "constants"
templates/Auto-resolved by template:Jinja2 files referenced as src: my.cnf.j2
files/Auto-resolved by copy:Static files referenced as src: repo.gpg
meta/main.ymlYesRole metadata + dependencies
tests/No (manual)Molecule tests for the role
library/YesCustom Python modules bundled with the role
filter_plugins/YesCustom Jinja2 filters
BASH — ansible-galaxy init
# create the standard skeleton
cd ./roles
ansible-galaxy init mysql

# what it generates
ls mysql/
# defaults  files  handlers  meta  README.md  tasks  templates  tests  vars

# verify
cat mysql/tasks/main.yml
# ---
# # tasks file for mysql

Three syntaxes to invoke a role — they're equivalent for simple cases:

YAML — three ways to call a role
---
# 1. Apply at the play level — runs roles BEFORE explicit tasks
- hosts: databases
  become: true
  roles:
    - common
    - mysql            # short form
    - role: monitoring # long form
      vars:
        metrics_port: 9100

  tasks:
    - name: Open metrics port
      ansible.posix.firewalld:
        port: 9100/tcp
        state: enabled

---
# 2. Mid-task using import_role (static — flattened at parse time)
- hosts: databases
  tasks:
    - name: Apply common role
      ansible.builtin.import_role:
        name: common

# 3. Mid-task using include_role (dynamic — applied at runtime)
- hosts: databases
  tasks:
    - name: Apply OS-specific role
      ansible.builtin.include_role:
        name: "os-{{{{ ansible_distribution | lower }}}}"

Every role can declare its own dependencies. Ansible resolves the chain and runs dependencies before the role itself. Useful when role A genuinely won't work without role B having configured something first.

YAML — roles/mysql/meta/main.yml
---
galaxy_info:
  author: deploy-team
  description: Install and configure MySQL 8 on OEL 8
  license: MIT
  min_ansible_version: "2.14"
  platforms:
    - name: EL
      versions:
        - "8"
  galaxy_tags:
    - database
    - mysql

dependencies:
  # roles below run BEFORE this one
  - role: common
  - role: firewall
    vars:
      firewall_open_ports:
        - 3306
  - role: epel
    when: ansible_os_family == "RedHat"
⚠ Warning: Dependencies are powerful but easy to abuse. Keep them few. If role app always wants nginx, that's a real dependency. If role app sometimes wants nginx, sometimes apache — let the calling playbook compose them. Don't bury the choice inside meta.

When a play says roles: [mysql], Ansible searches the following paths in order and uses the first match it finds:

  1. ./roles/<name>/ — relative to the playbook
  2. Directories listed in roles_path in ansible.cfg
  3. ~/.ansible/roles/ — your user's Galaxy install dir
  4. /etc/ansible/roles/ — system-wide install dir
INI — ansible.cfg roles_path
[defaults]
# colon-separated, like PATH
# Ansible will search each in order
roles_path = ./roles:./shared-roles:~/.ansible/roles
BASH — debug role discovery
# Show every role currently visible to ansible-galaxy
ansible-galaxy role list

# Run with -vv to see which directory Ansible loaded for each role
ansible-playbook site.yml -vv 2>&1 | grep "ROLE\|Loading"
# → Loading role 'mysql' from /home/deploy/work/proj/roles/mysql

# Show the dependency tree of a role
ansible-galaxy role list --include-dependencies geerlingguy.mysql
✅ Tip: Once you internalise the standard layout, every role you'll ever read or write becomes immediately legible — task list lives at tasks/main.yml, settings at defaults/main.yml, templates at templates/. No surprises.