The Problem
A playbook starts clean, then grows. Install packages, drop a dozen templates, register handlers, set defaults — soon a single site.yml is 400 lines and you are copy-pasting the nginx block into three other playbooks. The moment you need the same setup twice, a flat playbook stops scaling. Roles are Ansible's answer: a standard folder structure that packages tasks, templates, variables, and handlers into one reusable unit you can drop into any playbook.
A role is simply a directory with a well-known structure. Ansible knows where to find tasks, handlers, templates, and variables by convention, so you reference the role by name and it just works.
The Standard Role Directory Layout
Every role follows the same layout. You rarely need all of these folders — include only the ones you use.
roles/
└── nginx/
├── tasks/
│ └── main.yml # the main list of tasks (entry point)
├── handlers/
│ └── main.yml # handlers, e.g. "restart nginx"
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates rendered onto the host
├── files/
│ └── index.html # static files copied as-is
├── vars/
│ └── main.yml # high-precedence variables
├── defaults/
│ └── main.yml # low-precedence default variables
└── meta/
└── main.yml # role metadata and dependencies
| Directory | Role |
|---|---|
| <code class="inline-code">tasks/</code> | The ordered tasks. <code class="inline-code">main.yml</code> is the entry point Ansible runs. |
| <code class="inline-code">handlers/</code> | Tasks triggered by <code class="inline-code">notify</code>, typically restarts. |
| <code class="inline-code">templates/</code> | Jinja2 (<code class="inline-code">.j2</code>) files rendered with variables, then placed on the host. |
| <code class="inline-code">files/</code> | Static files copied verbatim with the <code class="inline-code">copy</code> module. |
| <code class="inline-code">vars/</code> | Variables with higher precedence — for values that rarely change. |
| <code class="inline-code">defaults/</code> | Variables with the lowest precedence — easy for users to override. |
| <code class="inline-code">meta/</code> | Author info and <code class="inline-code">dependencies</code> on other roles. |
A Realistic Role: Install and Configure nginx
Here is the heart of a working nginx role. Start with the defaults a user can override:
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_processes: auto
nginx_server_name: localhost
The tasks install the package, render the config from a template, and notify a handler on change:
# roles/nginx/tasks/main.yml
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy nginx config from template
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Restart nginx
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
The handler only fires when the template task reports changed, so nginx restarts only when its config actually changes:
# roles/nginx/handlers/main.yml
---
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
The template uses the role's variables:
# roles/nginx/templates/nginx.conf.j2
worker_processes {{ nginx_worker_processes }};
http {
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
}
}
Using Roles in a Playbook
Once the role exists, a playbook becomes short and declarative. You list roles instead of inlining dozens of tasks:
---
- name: Set up web tier
hosts: web
become: true
roles:
- common
- role: nginx
vars:
nginx_port: 8080
nginx_server_name: app.example.com
The same nginx role can be applied to a different host group with different variables — that is the entire point of roles. Logic lives in one place; configuration varies per use.
Ansible Galaxy: Don't Reinvent Common Roles
Ansible Galaxy is a public registry of community roles and collections. Before writing a role for a well-known tool, check whether a maintained one already exists.
# Install a role from Galaxy
ansible-galaxy role install geerlingguy.nginx
# Scaffold a new empty role with the standard layout
ansible-galaxy role init roles/myrole
# Install roles pinned in a requirements file
ansible-galaxy install -r requirements.yml
A requirements.yml pins versions so every teammate and CI run gets the same role:
---
roles:
- name: geerlingguy.nginx
version: "3.1.4"
Variable Precedence Basics
When the same variable is set in multiple places, Ansible applies a defined precedence order. The essentials to remember:
defaults/main.ymlhas the lowest precedence — designed to be overridden.vars/main.ymlhas higher precedence than defaults.- Variables passed when calling the role (as shown above) override the role's own
vars. -e/--extra-varson the command line wins over everything.
The practical rule: put sensible, overridable values in defaults/, reserve vars/ for things that should rarely change, and let callers override through role parameters.
Common Mistakes to Avoid
- Putting values in
vars/that users should tune. Those are hard to override. Tunable values belong indefaults/. - Hard-coding paths and hostnames in tasks. Promote them to variables so the role stays reusable.
- Giant catch-all roles. A role should do one job (nginx, postgres, firewall). A "webserver-and-db-and-monitoring" role defeats reuse.
- Forgetting
notify/handler wiring. Without it, a config change won't trigger a restart and the running service stays stale. - Skipping version pins in
requirements.yml. Unpinned Galaxy roles can change under you and break a previously working run.
FAQ
What is the difference between <code class="inline-code">vars/</code> and <code class="inline-code">defaults/</code>?
Both hold variables, but defaults/main.yml has the lowest precedence and is meant to be overridden by users, while vars/main.yml has higher precedence for values that should rarely change. As a rule, put tunable settings in defaults/ and internal constants in vars/.
How do I reuse a role across multiple playbooks?
Place the role under a roles/ directory (or install it from Galaxy) and reference it by name in the roles: section of any playbook. The same role can target different host groups with different variables, so the logic is written once and configured per use.
What is Ansible Galaxy?
Ansible Galaxy is a public registry of reusable roles and collections maintained by the community. You install content with ansible-galaxy install, pin versions in a requirements.yml, and avoid rewriting automation for common tools.
Do I have to create every role directory?
No. Ansible only reads the directories a role actually uses. A simple role might contain just tasks/main.yml. Add handlers/, templates/, defaults/, and the rest only when you need them.