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.