This is the second post in our Ansible fundamentals series. Start with Ansible 101 if you're joining partway through.
The playbook from our last post worked well for one specific job — configuring web servers — defined entirely in one file. The moment a second playbook also needs "install and configure nginx," maybe with slightly different settings, the honest options are: copy those tasks into the new playbook and let them slowly diverge, or extract them into something reusable. A role is Ansible's answer for the second option.
What a role actually is
A role is a directory structure with a fixed, predictable layout — Ansible knows to look for specific files in specific places, so a role doesn't need to declare its own structure the way a playbook does.
roles/
nginx/
tasks/
main.yml
handlers/
main.yml
templates/
nginx.conf.j2
defaults/
main.yml
files/
index.html
# roles/nginx/tasks/main.yml
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true# roles/nginx/handlers/main.yml
---
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restartedTwo new concepts appear here that didn't exist in the single-file playbook version:
notifyand handlers. A handler only runs if a task that notifies it actually reportedchanged. Here, nginx only gets restarted when its configuration actually changed — not on every single playbook run, which matters for anything where an unnecessary restart has a real cost (a brief service interruption, a load balancer health check flapping).defaults/main.ymlholds variables a role's caller is expected to be able to override — the role's equivalent of a Terraform module's variables with sensible defaults, covered in our Terraform variables post if you want the direct comparison.
Calling a role from a playbook
# site.yml
---
- name: Configure web servers
hosts: web
become: true
roles:
- nginxThat's the entire playbook now. Every detail of how to configure nginx lives inside the role; the playbook's job is reduced to deciding which roles apply to which hosts.
# site.yml — a more realistic multi-role example
---
- name: Configure web servers
hosts: web
become: true
roles:
- common
- nginx
- monitoring
- name: Configure database servers
hosts: db
become: true
roles:
- common
- postgresql
- monitoringRoles compose. common (base OS hardening, standard packages, an internal monitoring agent) applies to every host; nginx and postgresql are specific to what that group of hosts actually runs. This is the shape most real Ansible codebases converge on: a small number of focused, single-purpose roles, combined per host group in the playbook itself.
Making a role actually configurable
# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_client_max_body_size: "1m"{# roles/nginx/templates/nginx.conf.j2 #}
worker_processes {{ nginx_worker_processes }};
events {
worker_connections {{ nginx_worker_connections }};
}
http {
client_max_body_size {{ nginx_client_max_body_size }};
# ...
}A caller who needs different values overrides them without touching the role itself:
- hosts: web
become: true
roles:
- role: nginx
vars:
nginx_worker_connections: 4096
nginx_client_max_body_size: "20m"This is the same design principle we covered for Terraform modules: a good role's interface is its variables, not its internals. A caller who needs to reach inside roles/nginx/tasks/main.yml to get the behavior they want means the role's defaults and variables don't yet cover what people actually need from it.
Where roles come from beyond your own project
ansible-galaxy is Ansible's equivalent of the Terraform Registry — a public catalog of community and vendor-maintained roles.
ansible-galaxy install geerlingguy.nginxFor genuinely standard, well-trodden configuration (a properly hardened PostgreSQL install, a standard Docker Engine setup), a well-maintained Galaxy role is usually a better starting point than writing the equivalent from scratch — the same argument we made for reaching into the Terraform Registry before writing a custom module. Reserve custom, in-house roles for what's genuinely specific to your own infrastructure and applications.
What to actually remember from this post
- A role is a fixed directory structure Ansible recognizes automatically —
tasks/,handlers/,templates/,defaults/,files/— not an arbitrary convention you invent per project. - Handlers only run on actual change, via
notify— this is what prevents an unnecessary service restart on every single run. defaults/main.ymlis a role's public interface — design it the way you'd design any reusable module's inputs.- Reach for Ansible Galaxy for standard configuration; write custom roles for what's genuinely specific to your own systems.
Next in the series: Ansible Variables, Facts, and Templates, where we go deeper on exactly how Jinja2 templating and variable precedence actually work.
