<aside> 🧭

Module 05 · Roles & Reusability

Packaging tasks, handlers, variables and templates into something a team can share, version and reuse. This is the boundary between writing playbooks and building automation other people depend on.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–04. You now have every ingredient a role contains — this module is about the container.

</aside>


Part A · What a role is

A1 · The problem roles solve

<aside> 📖

Official docs: Roles · Re-using files and roles · ansible-galaxy CLI

</aside>

<aside> 📦

The analogy. Think of flat-pack furniture. A wardrobe arrives as one box, and inside are the panels, the screws, the little hex key and an instruction booklet — everything the wardrobe needs, and nothing belonging to the bookshelf. You can carry that box to a completely different house and it still works, because it never assumed anything about the room.

A role is that box: its tasks, templates, files, handlers and default settings travel together. And the test of a well-packed one is the same as with furniture — a role that also tries to assemble the bookshelf, or that only works if the bookshelf is already there, is a badly packed box.

</aside>

A single-file playbook works until it does not. The failure is predictable:

<aside> ❌

One 800-line site.yml

Nginx, PostgreSQL, monitoring and firewall config all interleaved.

Nobody can find anything. Two people cannot edit it without conflicting. Nothing can be reused on the next project. Testing means running the whole thing.

</aside>

<aside> ✅

Four roles, one thin site.yml

Each role owns one concern, has its own defaults, its own handlers, its own templates.

Independently reviewable, independently testable, reusable across projects, and shareable through Galaxy.

</aside>

A role is a directory with a fixed layout. That is the whole idea — Ansible knows where to look for each kind of content, so you do not have to specify paths.

# site.yml becomes this thin
---
- name: Configure web tier
  hosts: web
  become: true
  roles:
    - common
    - nginx
    - monitoring

A2 · The directory structure

<aside> 🗃️

The analogy. Think of how a kitchen is laid out. Knives live in a knife drawer, spices in a rack, pans in a low cupboard. Nobody writes any of this down, and yet you can walk into a stranger's kitchen and find the cutlery in about four seconds. That shared convention is what lets you cook in someone else's house without a guided tour.

A role's directories are that convention. Because templates/ always means templates and tasks/main.yml is always where things start, Ansible can find files without being told where they are — which is exactly why src: nginx.conf.j2 needs no path at all. It is also why another engineer can open your role and know their way around it immediately.

</aside>

roles/nginx/
  defaults/main.yml      <- default variables. LOWEST precedence (level 2). Put almost everything here
  vars/main.yml          <- internal constants. HIGH precedence (level 15). Use sparingly
  tasks/main.yml         <- the entry point. What the role does
  handlers/main.yml      <- handlers this role provides
  templates/             <- Jinja2 templates. Found automatically by the template module
  files/                 <- static files. Found automatically by copy and script
  meta/main.yml          <- dependencies, galaxy metadata, allow_duplicates
  meta/argument_specs.yml  <- role argument validation (2.11+). Underused and excellent
  library/               <- custom modules only this role needs
  module_utils/          <- shared Python for those modules
  filter_plugins/        <- custom Jinja2 filters
  tests/                 <- test playbook and inventory
  README.md              <- what it does, every variable, an example. Not optional in a team

<aside> 🔑

Every directory is optional, and main.yml is the magic filename. Ansible automatically loads tasks/main.yml, handlers/main.yml, defaults/main.yml, vars/main.yml and meta/main.yml. Any other filename in those directories is ignored unless you explicitly include_tasks it.

So a role with only tasks/main.yml is a perfectly valid role. Directories you do not need simply do not exist.

</aside>

Splitting a large role

roles/nginx/tasks/
  main.yml           <- imports the others, in order
  install.yml
  configure.yml
  service.yml
# roles/nginx/tasks/main.yml
---
- ansible.builtin.import_tasks: install.yml
- ansible.builtin.import_tasks: configure.yml
- ansible.builtin.import_tasks: service.yml

<aside> 💡

Note the paths are bare filenames. Inside a role, import_tasks: install.yml resolves relative to tasks/. The same convenience applies to template: src=nginx.conf.j2 finding templates/nginx.conf.j2, and copy: src=index.html finding files/index.html. That path resolution is one of the main practical benefits of the role layout.

</aside>