Roles & Templates
Package reusable logic into roles, render configs with Jinja2 templates, and trigger restarts with handlers
Roles & Templates
Once a playbook has more than ~20 tasks, you'll want to break it apart. Roles are the unit of reuse — directories that bundle tasks, variables, files, templates, and handlers under a single name.
Anatomy of a Role
roles/nginx/
├── defaults/
│ └── main.yml # default variable values (lowest precedence)
├── vars/
│ └── main.yml # role variables (higher precedence)
├── tasks/
│ └── main.yml # tasks (the role's entrypoint)
├── handlers/
│ └── main.yml # handlers (referenced by `notify`)
├── files/
│ └── htpasswd # static files for `copy:`
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates for `template:`
├── meta/
│ └── main.yml # role metadata + dependencies
└── README.mdAnsible auto-loads the right file from the right directory — you never write absolute paths inside a role.
A Complete Role
Defaults
Sensible defaults the caller can override:
# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 10m
nginx_sites: [] # list of vhosts, supplied by callerTasks
The role's actual work:
# roles/nginx/tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Push main nginx.conf
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: '0644'
notify: restart nginx
- name: Push vhost configs
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
owner: root
mode: '0644'
loop: "{{ nginx_sites }}"
notify: reload nginx
- name: Enable vhosts
file:
src: "/etc/nginx/sites-available/{{ item.name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ nginx_sites }}"
notify: reload nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: trueHandlers
Triggered by the notify: lines above:
# roles/nginx/handlers/main.yml
---
- name: restart nginx
service:
name: nginx
state: restarted
- name: reload nginx
service:
name: nginx
state: reloadedTemplates
Jinja2 templates can interpolate variables and facts:
# roles/nginx/templates/nginx.conf.j2
user www-data;
worker_processes {{ nginx_worker_processes }};
events {
worker_connections {{ nginx_worker_connections }};
}
http {
sendfile on;
keepalive_timeout {{ nginx_keepalive_timeout }};
client_max_body_size {{ nginx_client_max_body_size }};
include /etc/nginx/sites-enabled/*.conf;
}And use loops, conditionals, and host introspection:
# roles/nginx/templates/vhost.conf.j2
upstream {{ item.name }}_backend {
{% for host in groups[item.backend_group] %}
server {{ hostvars[host]['ansible_host'] }}:{{ item.backend_port }};
{% endfor %}
}
server {
listen 80;
server_name {{ item.server_name }};
{% if item.tls_enabled | default(false) %}
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {{ item.server_name }};
ssl_certificate /etc/letsencrypt/live/{{ item.server_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ item.server_name }}/privkey.pem;
{% endif %}
location / {
proxy_pass http://{{ item.name }}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Using a Role
In a playbook, list roles to apply:
# playbooks/site.yml
---
- hosts: webservers
become: true
roles:
- common
- role: nginx
vars:
nginx_worker_processes: 4
nginx_sites:
- name: api
server_name: api.example.com
backend_group: webservers
backend_port: 3000
tls_enabled: true
- appPlays run roles top-to-bottom. Order matters when there are dependencies (e.g., common sets up the user nginx will run as).
Role Dependencies
If a role always needs another role, declare it in meta/main.yml:
# roles/app/meta/main.yml
---
dependencies:
- role: common
- role: dockerDependencies run before the role's own tasks. Use sparingly — explicit ordering in the playbook is usually clearer than implicit dependencies.
Where to Get Roles
You don't have to write every role. Ansible Galaxy hosts community roles:
ansible-galaxy role install geerlingguy.docker
ansible-galaxy collection install community.postgresql# requirements.yml — version-locked dependencies
---
roles:
- src: geerlingguy.docker
version: 7.0.0
collections:
- name: community.postgresql
version: 3.4.0ansible-galaxy install -r requirements.ymlPin versions. Without version:, you get whatever's latest on Galaxy — and a "no changes" PR might silently pull in a breaking upgrade. Treat role and collection versions like any other dependency.
When NOT to Make a Role
A role for "install one package" is over-engineering. Heuristics:
- Used in one playbook, never touched after? Inline tasks.
- Re-used across plays, or more than a handful of tasks? Role.
- Shared across teams or projects? Role with version, README, and example playbook.
What's Next
You can package reusable logic into roles with templates and handlers. The remaining patterns are the ones you need for production runs — secrets, dynamic inventories, conditionals, rolling deploys → Advanced Patterns.