Skip to content
DevOps devops iac 6 min read

Ansible Roles & Reuse

When your Ansible playbooks grow past a few dozen lines, they become hard to read, hard to test, and hard to share. An Ansible role (a self-contained, reusable bundle of tasks, files, templates, and variables) solves this by packaging everything needed to configure one piece of software into a tidy folder structure. Instead of one giant file that installs Nginx, configures PostgreSQL, and sets up a firewall all at once, you split the work into named roles you can mix, match, and reuse across projects. This page explains the standard role layout, why roles beat monolithic playbooks, and how to pull battle-tested community roles from Ansible Galaxy (Ansible’s public registry of shared roles).

What a role actually is

A role is just a directory with a fixed set of sub-folders. Ansible knows the meaning of each folder by its name, so you don’t have to wire anything up manually — you drop files in the right place and Ansible finds them. This convention-over-configuration approach is the whole point: every Ansible engineer on earth recognizes the same layout, which makes your automation instantly readable to others.

A role typically lives under a roles/ directory next to your playbook. Here is the standard structure for a role named nginx:

roles/
└── nginx/
    ├── tasks/
    │   └── main.yml          # the steps Ansible runs (entry point)
    ├── handlers/
    │   └── main.yml          # actions triggered by "notify" (e.g. restart nginx)
    ├── templates/
    │   └── site.conf.j2      # Jinja2 templates rendered with variables
    ├── files/
    │   └── index.html        # static files copied as-is
    ├── defaults/
    │   └── main.yml          # default variable values (lowest priority)
    ├── vars/
    │   └── main.yml          # role variables (higher priority)
    ├── meta/
    │   └── main.yml          # role metadata + dependencies
    └── README.md             # human docs for the role

You rarely need all of these. The only required folder is tasks/ with a main.yml. The rest are optional and created only when you need them.

What each folder is for

FolderPurposeWhen you need it
tasks/The list of steps to run. main.yml is the entry point.Always — this is the heart of the role.
handlers/Tasks run only when “notified” (e.g. restart a service after a config change).When a config edit should trigger a service reload.
templates/Jinja2 (.j2) files turned into real config using your variables.When config must change based on host or variable.
files/Static files copied verbatim to the target.When you ship a fixed file (a script, a cert).
defaults/Variables with the lowest priority — easy for users to override.Almost always — set sensible defaults here.
vars/Variables with higher priority, harder to override.For internal constants you don’t want changed.
meta/Galaxy info and a list of roles this one depends on.When publishing or chaining roles.

Put values users are meant to customize (ports, package versions, paths) in defaults/main.yml, not vars/main.yml. Defaults are the lowest-priority variables, so a user can override them from the playbook or inventory. Variables in vars/ are much harder to override and will surprise people.

Roles vs one big playbook

AspectGiant playbookRoles
ReadabilityHundreds of lines in one fileSmall, named, focused folders
ReuseCopy-paste between projectsReference the role from any playbook
TestingHard — all or nothingTest one role in isolation
SharingAwkwardPublish to Galaxy or a Git repo
Variable defaultsScatteredCentralized in defaults/

When to use roles: any time you configure a real service (Nginx, PostgreSQL, Docker), or any time a playbook grows past ~50 lines or you want to reuse logic. When NOT to: a one-off, throwaway task (e.g. a quick command on one server) is fine as a plain playbook — wrapping it in a role is over-engineering.

Creating a role from scratch

On Ubuntu 22.04/24.04 LTS, the ansible-galaxy command (bundled with Ansible) scaffolds the full folder layout for you. Run it from your project root:

cd ~/my-infra
ansible-galaxy init roles/nginx

Output:

- Role roles/nginx was created successfully

Now add the install steps to roles/nginx/tasks/main.yml:

---
- name: Install Nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true

- name: Deploy site config
  ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/sites-available/default
  notify: Restart nginx

- name: Allow HTTP through the firewall
  community.general.ufw:
    rule: allow
    port: "80"
    proto: tcp

Define the handler in roles/nginx/handlers/main.yml. A handler runs only if a task notifys it, and only once at the end of the play:

---
- name: Restart nginx
  ansible.builtin.service:
    name: nginx
    state: restarted

Then call the role from a thin top-level playbook (site.yml):

---
- hosts: webservers
  become: true
  roles:
    - nginx

Run it:

ansible-playbook -i inventory.ini site.yml

Output:

PLAY [webservers] **************************************************************

TASK [nginx : Install Nginx] ***************************************************
changed: [web01]

TASK [nginx : Deploy site config] **********************************************
changed: [web01]

RUNNING HANDLER [nginx : Restart nginx] ****************************************
changed: [web01]

PLAY RECAP *********************************************************************
web01  : ok=4  changed=3  unreachable=0  failed=0

Pulling community roles from Galaxy

Ansible Galaxy is a free public registry where the community shares pre-built roles. Instead of writing PostgreSQL setup yourself, you can install a maintained role and save hours. The best practice is to pin roles in a requirements.yml file so installs are reproducible:

---
roles:
  - name: geerlingguy.postgresql
    version: "3.5.2"
  - name: geerlingguy.docker
    version: "7.4.1"

Install everything listed:

ansible-galaxy install -r requirements.yml

Output:

Starting galaxy role install process
- downloading role 'postgresql', owned by geerlingguy
- extracting geerlingguy.postgresql to ~/.ansible/roles/geerlingguy.postgresql
- geerlingguy.postgresql (3.5.2) was installed successfully
- geerlingguy.docker (7.4.1) was installed successfully

Then use the installed role like any other:

---
- hosts: databases
  become: true
  roles:
    - role: geerlingguy.postgresql
      postgresql_databases:
        - name: appdb

Always pin a version: for Galaxy roles. Without a version, ansible-galaxy install grabs the latest release, so a future run can silently pull breaking changes and wreck a production deploy. Treat role versions like any other dependency.

Best Practices

  • Keep each role focused on one concern (one service or one job) — a role named everything defeats the purpose.
  • Put all user-tunable values in defaults/main.yml and document them in the role’s README.md.
  • Pin Galaxy role versions in requirements.yml and commit that file to Git for reproducible installs.
  • Use templates/ with Jinja2 for any config that varies by host; use files/ only for truly static content.
  • Use handlers for service restarts instead of restarting in a task, so the service reloads once at the end rather than on every change.
  • Keep your top-level site.yml thin — it should list which hosts get which roles, nothing more.
  • Run ansible-playbook --check (a dry run) before applying role changes to a production server.
Last updated June 15, 2026
Was this helpful?