A single long playbook works until the second server type appears. Roles are Ansible’s answer: a fixed directory
layout that bundles tasks, templates, handlers and default variables into one reusable unit. A common role can be applied
to every host; an nginx role only to the web tier.
This guide follows our playbook basics and keeps secrets safe with
Ansible Vault. We build an nginx role from scratch.
Why Roles
Roles remove copy-paste between playbooks and give each concern one home. The playbook shrinks to a list of hosts and roles.
- hosts: web
become: true
roles:
- common
- nginx

Know the Role Layout
Each directory has one job. Ansible loads tasks/main.yml automatically and finds templates, handlers and defaults by convention.
roles/nginx/
├── tasks/main.yml what to do
├── handlers/main.yml restart / reload actions
├── templates/nginx.conf.j2
├── defaults/main.yml overridable defaults
└── vars/main.yml fixed internal values

Scaffold with ansible-galaxy
Generate the skeleton, then delete directories you will not use to keep the role readable.
mkdir -p roles && cd roles
ansible-galaxy role init nginx
rm -rf nginx/tests nginx/meta/.galaxy_install_info
git add . && git commit -m 'Add nginx role skeleton'

Write Idempotent Tasks
Prefer purpose-built modules over shell: they detect the current state and only change what is needed, so running twice is safe.
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
tags: [nginx, packages]
- name: Render nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
validate: nginx -t -c %s
notify: Reload nginx
tags: [nginx, config]
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
validate: nginx -t -c %s refuses to install a broken config — the role can never take the web server down with a typo.
Templates and Handlers
A Jinja2 template turns variables into configuration. The notify line queues a handler, which runs once at the end of the play and only if the file changed.
worker_processes {{ nginx_worker_processes }};
events { worker_connections {{ nginx_worker_connections }}; }
http {
server_tokens off;
include /etc/nginx/conf.d/*.conf;
}
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded

Defaults, group_vars and Secrets
Put safe fallbacks in defaults/main.yml; override per environment in group_vars. Encrypt anything sensitive with Vault.
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_worker_connections: 4096

Lint and Dry-Run Before Applying
Catch style problems and mistakes before they touch a server. --check --diff shows what would change without changing it.
pip install ansible-lint
ansible-lint roles/nginx
ansible-playbook -i inventory site.yml --check --diff --limit staging

Share and Pin Roles with requirements.yml
Pin third-party roles to a version so a surprise upstream change cannot alter your servers, and read them before trusting them.
roles:
- name: geerlingguy.certbot
version: 5.1.0
ansible-galaxy role install -r requirements.yml

Quick Reference
- One concern per role; defaults overridable; secrets in Vault
- Modules over shell;
validateon config templates; lint and--checkfirst
Related tutorials
Diagrams are original illustrations by Gnome IT Solutions. Tutorial text © Gnome IT Solutions.