Ansible Deep Dive Part 5: Roles and Ansible Galaxy — Structuring Projects That Scale
A role is a self-contained, reusable bundle of tasks, handlers, templates, files, and default variables — everything Part 3 and Part 4 covered, packaged so it can be dropped into any playbook without copy-pasting. If you’re configuring more than one type of server, or sharing playbooks across projects, roles stop being optional pretty quickly.
The standard directory layout
ansible-galaxy init roles/nginx
produces:
roles/nginx/
tasks/main.yml # the task list — what Part 3 called "tasks:"
handlers/main.yml # handlers, same as before
templates/ # .j2 files, referenced by relative name
files/ # static files for the `copy` module
vars/main.yml # role-internal variables, high precedence
defaults/main.yml # role variables, LOWEST precedence — meant to be overridden
meta/main.yml # role metadata and dependencies
This layout is entirely convention, not magic — Ansible looks for these exact filenames because that’s what ansible-galaxy init scaffolds and what every role in existence follows, and deviating from it just makes your role confusing to anyone who’s used any other role before.
Using a role in a playbook
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- role: nginx
vars:
http_port: 8080
Everything from the nginx role’s tasks/main.yml runs as if it were written inline in the play, with that role’s defaults/ providing fallback values and the vars: block here overriding them for this specific playbook.
defaults/ vs vars/ — the distinction that trips people up
Both live inside a role and both set variables, but they sit at opposite ends of the precedence ladder. defaults/main.yml is the lowest-precedence source in the entire system — deliberately, so that anything else (playbook vars, inventory vars, -e on the command line) overrides it without a fight. vars/main.yml is much higher precedence, closer to the top — meant for values the role’s internals depend on that a user of the role generally shouldn’t be casually overriding.
Practical rule: anything you expect a consumer of your role to reasonably want to change goes in defaults/. Anything that’s an internal implementation detail of the role goes in vars/. Get this backwards and you’ll end up with a role that’s either impossible to customize or dangerously easy to break.
Ansible Galaxy: the community role and collection registry
ansible-galaxy collection install community.general
ansible-galaxy collection install cisco.ios
ansible-galaxy collection install fortinet.fortios
ansible-galaxy role install geerlingguy.nginx
Collections are the modern packaging unit — a bundle of modules, plugins, and roles, versioned and installed together (cisco.ios, fortinet.fortios, community.general are all collections). Roles installed directly via ansible-galaxy role install are the older, standalone unit, still widely used for community-maintained configuration roles like Jeff Geerling’s extensive library.
Before writing a role from scratch, it’s worth five minutes on galaxy.ansible.com checking whether a well-maintained one already exists — geerlingguy.nginx, geerlingguy.mysql, and similar cover the vast majority of common server software, actively maintained, tested against multiple OS families, and almost certainly more thorough than a first-pass internal role would be.
Pinning dependencies with requirements.yml
Installing collections and roles ad-hoc on whatever machine happens to run the playbook is how “works on my laptop” happens. Every real project should declare its dependencies explicitly:
# requirements.yml
collections:
- name: community.general
version: ">=8.0.0"
- name: cisco.ios
version: "5.3.0"
- name: fortinet.fortios
version: "2.3.6"
roles:
- name: geerlingguy.nginx
version: "3.1.4"
ansible-galaxy install -r requirements.yml
Check requirements.yml into version control alongside the playbooks that need it, and this single command reproduces the exact dependency set on any machine — a new team member’s laptop, a CI runner, a jump box — with no manual “oh, you’ll also need to install…” step in your onboarding docs.
Role dependencies
A role can declare other roles it needs run first, in meta/main.yml:
# roles/webapp/meta/main.yml
dependencies:
- role: common
- role: nginx
Ansible resolves and runs these automatically before webapp’s own tasks — useful for expressing “any role that needs a web server should just depend on the nginx role” rather than every playbook remembering to list both roles manually. I use this sparingly, though; deep dependency chains between roles get hard to reason about, and an explicit roles: list in the playbook is often clearer than an implicit one buried in meta/main.yml three files away.
Part 6 covers where variables actually come from when more than one of these sources disagrees — the full precedence order — and Ansible Vault, which is how secrets (API tokens, device passwords, TLS private keys) get to live in the same version-controlled repository as everything else without landing in plaintext on GitHub.