Ansible Roles: Complete Guide to Creating & Using Roles (2026)
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible roles. Create, structure, use, and share reusable roles with defaults, handlers, templates, and Galaxy integration.

What Are Ansible Roles?
Ansible roles are a way to organize automation content into reusable, self-contained units. Instead of writing everything in one massive playbook, roles let you break automation into logical components.
See also: Ansible Playbook Scanning Tools — 5 Best Tools for Secure Automation in 2026
Why Use Roles?
• Reusability — Write once, use across projects • Organization — Clean separation of concerns • Sharing — Publish to Ansible Galaxy • Testing — Test each role independently with Molecule • Maintainability — Smaller, focused files are easier to manageRole Directory Structure
roles/
└── webserver/
├── defaults/
│ └── main.yml # Default variables (lowest priority)
├── vars/
│ └── main.yml # Role variables (high priority)
├── tasks/
│ └── main.yml # Main task list
├── handlers/
│ └── main.yml # Handlers (e.g., restart services)
├── files/
│ └── nginx.conf # Static files to copy
├── templates/
│ └── vhost.conf.j2 # Jinja2 templates
├── meta/
│ └── main.yml # Role metadata & dependencies
├── tests/
│ ├── inventory
│ └── test.yml # Test playbook
└── README.md # Documentation
What Goes Where
| Directory | Purpose | Priority |
|-----------|---------|----------|
| defaults/ | Default values users can override | Lowest |
| vars/ | Internal role variables | High |
| tasks/ | The automation tasks | — |
| handlers/ | Event-triggered tasks | — |
| files/ | Static files to copy to hosts | — |
| templates/ | Jinja2 templates to render | — |
| meta/ | Dependencies, platforms, license | — |
See also: Ansible Development: Write Custom Modules, Plugins & Collections
Creating a Role
Using ansible-galaxy init
ansible-galaxy role init roles/webserver
Manual Example
roles/webserver/tasks/main.yml:
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy site config
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ domain }}"
notify: Reload nginx
- name: Enable site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ domain }}"
dest: "/etc/nginx/sites-enabled/{{ domain }}"
state: link
notify: Reload nginx
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
roles/webserver/handlers/main.yml:
---
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
roles/webserver/defaults/main.yml:
---
domain: example.com
listen_port: 80
document_root: "/var/www/{{ domain }}"
roles/webserver/templates/vhost.conf.j2:
server {
listen {{ listen_port }};
server_name {{ domain }};
root {{ document_root }};
location / {
try_files $uri $uri/ =404;
}
}
Using Roles in Playbooks
Basic Usage
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- webserver
With Variables
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- role: webserver
vars:
domain: mysite.com
listen_port: 8080
Multiple Roles
---
- name: Full server setup
hosts: all
become: true
roles:
- common # Base config (users, SSH, firewall)
- security # Hardening
- role: webserver # Nginx
vars:
domain: app.example.com
- monitoring # Prometheus node exporter
Conditional Roles
roles:
- role: mysql
when: "'databases' in group_names"
See also: Ansible Galaxy: Install, Create & Share Roles and Collections (Guide)
Roles vs Playbooks
| Aspect | Playbook | Role | |--------|----------|------| | Scope | Complete automation | Single component | | Reusability | Low (project-specific) | High (across projects) | | Variables | Inline or vars files | defaults/ and vars/ | | File organization | Single file or flat | Standard directory structure | | Sharing | Copy/paste | Galaxy, Git, or tarball | | Testing | Complex | Molecule (per role) |
Rule of thumb: If you'll use it more than once or it represents a logical component (database, webserver, monitoring), make it a role.
Role Dependencies
Define dependencies in meta/main.yml:
# roles/webserver/meta/main.yml
---
dependencies:
- role: common
- role: firewall
vars:
allowed_ports:
- 80
- 443
Dependencies run before the role's tasks.
defaults/ vs vars/
This is a common source of confusion:
# defaults/main.yml — Users CAN override these
http_port: 80 # Sensible default
max_connections: 100 # Tune per environment
# vars/main.yml — Internal, should NOT be overridden
nginx_package: nginx # Implementation detail
config_path: /etc/nginx # OS-specific constant
Variable precedence: defaults/ < inventory vars < playbook vars < vars/ < extra vars (-e).
Role Search Path
Ansible looks for roles in:
roles/ directory relative to the playbook
Paths in roles_path setting in ansible.cfg
~/.ansible/roles (Galaxy default)
/etc/ansible/roles (system default)
# ansible.cfg
[defaults]
roles_path = ./roles:~/.ansible/roles:/etc/ansible/roles
Best Practices
One role = one responsibility (don't mix nginx + MySQL in one role) Usedefaults/ for user-configurable values, vars/ for internals
Prefix variable names with the role name: webserver_port not port
Document your role in README.md (Galaxy renders this)
Test with Molecule before publishing
Tag your tasks for selective execution
Support multiple OS families using when: ansible_os_family
Pin dependency versions in meta/main.yml
FAQ
How do I install a role from Galaxy?
ansible-galaxy role install geerlingguy.docker
Where are roles stored?
Default:~/.ansible/roles/. Set custom path with roles_path in ansible.cfg.
Can a role include other roles?
Yes, viameta/main.yml dependencies or include_role/import_role in tasks.
What is the difference between include_role and import_role?
import_role is static (parsed at load time, supports tags). include_role is dynamic (parsed at runtime, supports loops and conditionals).
Conclusion
Roles are the foundation of maintainable Ansible automation. Start using roles early in your projects — the upfront structure pays off immediately in reusability and clarity.
For more tutorials, visit AnsiblePilot.
Create a Role
# Using ansible-galaxy
ansible-galaxy role init webserver
# Or manually
mkdir -p roles/webserver/{tasks,handlers,defaults,vars,templates,files,meta}
Role Directory Structure
roles/webserver/
├── defaults/main.yml # Default variables (easily overridden)
├── vars/main.yml # Role variables (high priority)
├── tasks/main.yml # Main task list
├── handlers/main.yml # Event handlers
├── templates/ # Jinja2 templates
│ ├── nginx.conf.j2
│ └── vhost.conf.j2
├── files/ # Static files
│ └── index.html
├── meta/main.yml # Role metadata & dependencies
├── tests/ # Test playbooks
│ ├── inventory
│ └── test.yml
└── README.md
Use Roles in Playbooks
# Simple
- hosts: webservers
roles:
- webserver
- monitoring
# With variables
- hosts: webservers
roles:
- role: webserver
vars:
http_port: 8080
enable_ssl: true
# Conditional
- hosts: all
roles:
- role: docker
when: install_docker | default(false)
Role with Tasks, Handlers, Templates
# roles/webserver/defaults/main.yml
http_port: 80
server_name: localhost
document_root: /var/www/html
# roles/webserver/tasks/main.yml
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
become: true
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: "nginx -t -c %s"
notify: restart nginx
become: true
- name: Deploy site
ansible.builtin.copy:
src: index.html
dest: "{{ document_root }}/index.html"
become: true
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
become: true
# roles/webserver/handlers/main.yml
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
become: true
Role Dependencies
# roles/webapp/meta/main.yml
dependencies:
- role: common
- role: webserver
vars:
http_port: 8080
- role: firewall
vars:
open_ports: [80, 443, 8080]
Include Role Dynamically
# Inline (dynamic)
- include_role:
name: webserver
vars:
http_port: 9090
# Static import
- import_role:
name: webserver
# Specific tasks file
- include_role:
name: webserver
tasks_from: ssl-setup
Galaxy Roles
# Install
ansible-galaxy role install geerlingguy.docker
ansible-galaxy role install -r requirements.yml
# requirements.yml
roles:
- name: geerlingguy.docker
version: "7.0.0"
- name: geerlingguy.nginx
- name: custom_role
src: git+https://github.com/org/role.git
version: main
Role Testing with Molecule
cd roles/webserver
molecule init scenario
molecule test
Best Practices
• One role = one responsibility (webserver, database, monitoring) • Use defaults/ for user-configurable variables • Use vars/ for internal constants • Document variables in README.md • Tag your tasks for selective execution • Test with Molecule before sharing • Version your roles in requirements.ymlFAQ
Role vs playbook — when to use which?
Roles for reusable components (install nginx, configure database). Playbooks for orchestration (deploy full application stack using multiple roles).
How do I override role defaults?
Set variables in play vars, group_vars, host_vars, or extra vars (-e). All have higher priority than role defaults.
Can roles call other roles?
Yes — via meta/main.yml dependencies (auto-run) or include_role/import_role in tasks (manual).
Create a Role
ansible-galaxy role init my_role
Role Structure
my_role/
├── defaults/main.yml # Default variables (lowest priority)
├── vars/main.yml # Role variables (higher priority)
├── tasks/main.yml # Main task list
├── handlers/main.yml # Handlers
├── templates/ # Jinja2 templates
├── files/ # Static files
├── meta/main.yml # Dependencies, Galaxy metadata
└── README.md
Use a Role
# In playbook
- hosts: webservers
roles:
- nginx
- { role: myapp, app_port: 8080 }
- role: monitoring
when: enable_monitoring
Role Defaults
# defaults/main.yml — users can override these
app_port: 8080
app_workers: 4
app_log_level: info
app_user: appuser
Role Tasks
# tasks/main.yml
- name: Install packages
ansible.builtin.apt:
name: "{{ app_packages }}"
state: present
become: true
- name: Deploy config
ansible.builtin.template:
src: config.conf.j2
dest: "/etc/{{ app_name }}/config.conf"
notify: restart app
- name: Ensure service running
ansible.builtin.service:
name: "{{ app_name }}"
state: started
enabled: true
become: true
Role Dependencies
# meta/main.yml
dependencies:
- role: common
- role: geerlingguy.docker
vars:
docker_edition: ce
include_role vs roles
# Static (roles section) — runs before tasks
- hosts: all
roles:
- nginx
# Dynamic (include_role) — runs inline with tasks
- hosts: all
tasks:
- debug: msg="Before role"
- include_role:
name: nginx
when: install_nginx
- debug: msg="After role"
Role with Tags
- hosts: all
roles:
- role: webserver
tags: [web, deploy]
Organize Multiple Roles
project/
├── site.yml
├── inventory/
├── group_vars/
└── roles/
├── common/
├── webserver/
├── database/
└── monitoring/
FAQ
defaults vs vars?
defaults/ has lowest priority — users easily override. vars/ has higher priority — harder to override. Use defaults/ for most role variables.
How to pass variables to a role?
roles:
- role: myapp
vars:
app_port: 9090
Can a role include other roles?
Yes — via meta/main.yml dependencies or include_role/import_role in tasks.
Related Articles
• publishing collections to Ansible Galaxy • using ansible.builtin.template effectively • listen-based handlers in Ansible • Ansible conditional patterns • inventory configuration in AnsibleCategory: installation