As enterprise automation grows to thousands of lines of YAML, placing all tasks, variables, handlers, and templates in single files creates unmaintainable monoliths.
Ansible Roles provide a standardized, modular directory structure that encapsulates tasks, variables, handlers, templates, and files into clean, reusable automation packages.
Topic 1: Standard Ansible Role Directory Layout
When you generate a role using ansible-galaxy init roles/my_role, Ansible creates a standardized directory tree:
roles/nginx_server/
├── defaults/
│ └── main.yml # Lowest-precedence default variables (users CAN override)
├── vars/
│ └── main.yml # High-precedence internal role constants (users SHOULD NOT override)
├── tasks/
│ └── main.yml # Core execution tasks (can include sub-task files)
├── handlers/
│ └── main.yml # Service notification handlers
├── templates/
│ └── nginx.conf.j2 # Jinja2 template files (referenced without directory path)
├── files/
│ └── index.html # Static files to copy (referenced without directory path)
├── meta/
│ └── main.yml # Role metadata, author info, and role dependencies
└── README.md # Documentation for role usage
[!NOTE] Directory Auto-Discovery: When a role executes, Ansible automatically searches inside the role’s
templates/folder for template tasks andfiles/folder for copy tasks. You do not write relative paths likesrc: roles/nginx_server/templates/nginx.conf.j2— writesrc: nginx.conf.j2directly!
Topic 2: Consuming Roles in Playbooks
You can invoke roles in playbooks using three primary methods:
1. Classic roles: Directive (Static Loading at Play Start)
The standard method to apply roles to host groups:
---
- name: Deploy Production Web Tier
hosts: webservers
become: true
roles:
- role: common_base
- role: nginx_server
vars:
nginx_http_port: 8080 # Overrides role defaults/main.yml
2. Dynamic include_role (Evaluated at Runtime)
Loads a role dynamically inside a task list based on runtime conditionals:
tasks:
- name: Conditionally load database tuning role
ansible.builtin.include_role:
name: postgresql_tuning
when: db_performance_mode == "high"
3. Static import_role (Parsed at Playbook Pre-processing)
Imports a role statically before playbook execution begins:
tasks:
- name: Import security compliance role
ansible.builtin.import_role:
name: rhel_hardening
Topic 3: Role Dependencies (meta/main.yml)
Roles can declare dependencies on other roles inside meta/main.yml. When the parent role runs, Ansible automatically executes all dependent roles first:
# roles/app_server/meta/main.yml
---
galaxy_info:
author: DevOps Team
description: Application Server Role
license: MIT
min_ansible_version: "2.14"
dependencies:
- role: common_security
- role: java_runtime
vars:
java_version: "17"
Topic 4: Ansible Galaxy & Requirements Management (requirements.yml)
Ansible Galaxy is the public community hub for sharing Ansible Roles and Collections.
- Installing Roles via CLI:
ansible-galaxy install geerlingguy.nginx -p ./roles - Managing Project Dependencies (
roles/requirements.yml): Commit arequirements.ymlfile to version control listing all external role dependencies and pinned Git tags:
# roles/requirements.yml
roles:
# Role from Ansible Galaxy community hub
- name: geerlingguy.nginx
src: geerlingguy.nginx
version: 3.2.0
# Role from private corporate Git repository
- name: internal.security_hardening
src: git@github.com:myorg/ansible-role-security.git
scm: git
version: v1.4.0
# Install all required roles into project roles directory
ansible-galaxy install -r roles/requirements.yml -p ./roles
[!TIP] Red Hat Best Practice: One Git Repository Per Role: In enterprise teams, place each role into its own dedicated Git repository. Use
requirements.ymlto pull specific tagged versions into playbooks, guaranteeing auditable and repeatable deployments across environments.
Common mistake: Writing a role that reaches into another role’s internal variables, or that only works when called in a specific order. It stops being reusable the first time somebody calls it on its own. Everything a role needs belongs in defaults/main.yml as a documented interface, and anything it truly requires belongs in meta/main.yml as a dependency.