Ansible Course
Roles Introduction
In this lesson
An Ansible role is a
standardised, reusable unit of automation that packages tasks, variables, handlers,
templates, files, and defaults together in a defined directory structure. Where a
playbook is a single-file script that runs tasks against hosts, a role is a
self-contained module of automation logic that any playbook can call by name. Roles
exist to solve the maintainability problem that grows with every playbook: rather than
copying Nginx tasks across five different playbooks and keeping them in sync, you write
an nginx role once and call it from any playbook that needs it. The role
is the standard unit of sharing in the Ansible community — every role on Ansible Galaxy
follows the same structure, which means you can understand any role you download within
minutes of opening it.
Roles vs Flat Playbooks
Roles are not a replacement for playbooks — they work alongside them. The playbook decides which hosts to target and which roles to apply. The role provides the reusable logic. This separation of concerns is what makes large Ansible projects maintainable.
Role Directory Structure
Every Ansible role follows the same
directory layout. Ansible loads each subdirectory automatically — you never need to
include or import files from within a role explicitly. This
convention is what makes every role immediately navigable.
Role subdirectory reference
tasks/main.yml
The entry point
for all role tasks. Ansible executes this file automatically when the role is
called. The only required subdirectory.
defaults/main.yml
Default variable
values — the lowest precedence of any variable source. These are the values
the role uses if nothing else overrides them. Always define role defaults here,
not in vars/.
vars/main.yml
Internal role
variables with high precedence — values here override group_vars
and inventory variables. Use for constants the role defines internally, not
for values the caller should be able to override.
handlers/main.yml
Handlers scoped
to this role. They can be notified by any task in the role and run at the end
of the play like regular handlers.
templates/
Jinja2 template
files. The template module in role tasks can reference these
files by name alone — no path needed, Ansible finds them automatically.
files/
Static files for
the copy module. Like templates, they can be referenced by
name alone — no path required.
meta/main.yml
Role metadata —
author, description, supported platforms, and most importantly, role
dependencies. Ansible reads this to automatically apply dependency roles
before the current role runs.
The IKEA Flatpack Analogy
An Ansible role is like an IKEA flatpack. The
box contains everything needed to assemble one specific piece of furniture — parts,
hardware, and instructions — in a standardised package that anyone can pick up and
use. You do not need to know how it was designed; you just follow the standard
assembly process. The role's directory structure is the box layout — always the same,
always in the same place, immediately recognisable to anyone who has used a role before.
And like IKEA, you can customise it with optional accessories
(defaults variables) without modifying the core design.
Creating a Role
The fastest way to create the role
skeleton is with ansible-galaxy role init. It generates the full
directory structure with placeholder files, saving you the manual work and ensuring
nothing is missed.
# Create a new role skeleton in the current directory
ansible-galaxy role init nginx
# Create inside a specific roles/ directory
ansible-galaxy role init --init-path roles/ nginx
- Role nginx was created successfully
roles/nginx/
├── README.md
├── defaults/
│ └── main.yml
├── files/
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── tasks/
│ └── main.yml
├── templates/
├── tests/
│ ├── inventory
│ └── test.yml
└── vars/
└── main.ymlWhat just happened?
Ansible created the entire role skeleton with
empty placeholder files in every standard directory. You now have a valid, importable
role structure — start by editing tasks/main.yml to add your tasks,
then defaults/main.yml to define configurable defaults. All other
files are optional and can be left empty or deleted if unused.
Annotated roles/nginx/tasks/main.yml
---
# roles/nginx/tasks/main.yml
# No 'hosts:' or 'become:' here — those are set in the playbook that calls this role
- name: Install Nginx
ansible.builtin.package:
name: "{{ nginx_package }}" # variable from defaults/main.yml
state: present
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2 # found automatically in templates/ — no path needed
dest: /etc/nginx/nginx.conf
mode: "0644"
notify: Reload Nginx # handler in handlers/main.yml
- name: Deploy virtual host configurations
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/nginx/conf.d/{{ item.name }}.conf"
mode: "0644"
loop: "{{ nginx_vhosts }}" # variable from defaults/main.yml
notify: Reload Nginx
- name: Ensure Nginx is started and enabled
ansible.builtin.service:
name: "{{ nginx_package }}"
state: started
enabled: true
Annotated roles/nginx/defaults/main.yml
---
# roles/nginx/defaults/main.yml
# These are the LOWEST priority variables — callers can override every one of these.
nginx_package: nginx
nginx_user: www-data
nginx_worker_processes: "{{ ansible_processor_vcpus | default(1) }}"
nginx_worker_connections: 1024
nginx_port: 80
nginx_vhosts:
- name: default
server_name: "{{ ansible_fqdn }}"
root: /var/www/html
roles/nginx/handlers/main.yml
---
# roles/nginx/handlers/main.yml
- name: Reload Nginx
ansible.builtin.service:
name: "{{ nginx_package }}"
state: reloaded
- name: Restart Nginx
ansible.builtin.service:
name: "{{ nginx_package }}"
state: restarted
Using Roles in Playbooks
There are three ways to use roles in a playbook, each with different timing and scoping behaviour. Understanding which to use in which context prevents subtle ordering bugs.
roles: key
Classic method.
Roles listed under roles: run before any tasks in the play. Most
common and recommended for standard provisioning.
include_role
Dynamic import
at runtime. The role is loaded when the task executes — allows conditional role
inclusion using when:.
import_role
Static import
at parse time. Like include_role but processed before the play runs
— supports tags on role tasks.
---
# Method 1 — roles: key (most common)
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx # calls roles/nginx/tasks/main.yml
- role: nodejs # alternative verbose form
vars:
node_version: "20" # override role defaults for this play
- role: app_deploy
tags: deploy # tag the entire role
---
# Method 2 — include_role (dynamic — supports when:)
- name: Configure servers conditionally
hosts: all
become: true
tasks:
- name: Install Nginx on web servers only
ansible.builtin.include_role:
name: nginx
when: "'webservers' in group_names"
- name: Install PostgreSQL on DB servers only
ansible.builtin.include_role:
name: postgresql
when: "'databases' in group_names"
---
# Method 3 — import_role (static — tags work on role tasks)
- name: Deploy application
hosts: appservers
become: true
tasks:
- name: Run application role
ansible.builtin.import_role:
name: app_deploy
tags: deploy # tags propagate into role tasks
Role Dependencies
A role can declare that it depends on
other roles using the meta/main.yml file. Ansible automatically runs
dependency roles first — before the dependent role executes. This is how complex
roles express their prerequisites without requiring the playbook author to know
the exact order.
# roles/app_deploy/meta/main.yml
---
galaxy_info:
author: yourname
description: Deploys the application to web servers
license: MIT
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions: ["22.04", "20.04"]
dependencies:
# These roles run BEFORE app_deploy, automatically
- role: nginx # ensure Nginx is installed first
vars:
nginx_port: 8080 # override nginx defaults for this dependency
- role: common # common base configuration
# no variable overrides — use common's defaults
When a playbook calls
app_deploy, Ansible resolves the full dependency tree first: it runs
common, then nginx (with the port override), then
app_deploy. The playbook author does not need to know or specify
this order — the role itself encodes its prerequisites.
Put Overridable Variables in defaults/, Not vars/
This is the most common role
authoring mistake. Variables in vars/main.yml have very high precedence
— they override group_vars, host_vars, and play
vars:. If you put your role's configurable options in vars/,
callers cannot override them through normal variable mechanisms. Always put
configurable options in defaults/main.yml — which has the lowest
precedence of any variable source — and reserve vars/ for internal
constants that should never be overridden.
Key Takeaways
tasks/main.yml is required — all other
subdirectories are optional. Start with just tasks and defaults, and add
handlers, templates, and files as the role grows.
defaults/
— these have the lowest variable precedence and can be overridden by any
caller through inventory variables, play vars, or extra vars.
templates/ and
files/ directories, so no path prefix is needed in task
src: parameters.
meta/main.yml run automatically
— Ansible resolves the full dependency tree before running the role, so the
playbook author does not need to specify the correct order manually.
include_role for conditional role application
— the roles: key in a play cannot use when: conditions,
but include_role inside a task list can, enabling roles to be
applied only to the hosts that need them.
Teacher's Note
Run
ansible-galaxy role init nginx, move your Lesson 13 Nginx tasks into
roles/nginx/tasks/main.yml, move your template into
roles/nginx/templates/, and call the role from a two-line playbook.
The moment the playbook shrinks from 40 lines to 5, roles stop being a concept and
become the obvious way to write Ansible.
Practice Questions
1. What command generates a new role skeleton with all standard subdirectories and placeholder files?
2. In which role file should you define configurable variables that callers can override through inventory or play variables?
3. In which role file do you declare the other roles that must run before this role executes?
Quiz
1. What is the key difference between
variables defined in defaults/main.yml and
vars/main.yml?
2. A role's meta/main.yml
lists two dependency roles. What happens when a playbook calls this role?
3. You want to apply the
nginx role only to hosts in the webservers group,
even though your play targets all. How do you achieve this?
Up Next · Lesson 24
Creating and Using Roles
Build a complete, production-ready role from scratch — structuring tasks, vars, templates, and handlers — then call it from a multi-role site.yml that provisions a full server stack.