Roles — Organizing Playbooks for Reuse

Ansible

Chapter 7 · Roles — Organizing Playbooks for Reuse

Everything so far — tasks, handlers, variables, templates — has lived in one or two files. That works for a small example; it doesn't scale. This chapter is Ansible's real answer: roles, a standard way to package each concern into its own self-contained, reusable unit.

The Problem — One Big Playbook Doesn't Scale

As a playbook grows — more tasks, more handlers, more variables, more templates, all covering genuinely different concerns — it becomes hard to reuse any single piece elsewhere, and hard to find anything in a file that keeps growing. Roles solve this by giving each concern its own directory, structured the same predictable way every time.

The Role Directory Structure

roles/ nginx/ tasks/ main.yml handlers/ main.yml templates/ nginx.conf.j2 vars/ main.yml defaults/ main.yml files/ meta/ main.yml
  • tasks/main.yml — the role's own task list, loaded automatically by convention, no explicit reference needed anywhere
  • handlers/main.yml — handlers scoped to this role, working exactly as ansible1-6 described
  • templates/.j2 files, referenced by relative filename from the role's own tasks
  • defaults/main.yml — deliberately the lowest-precedence variables per ansible1-5's own order, meant to be overridden by whoever uses the role
  • vars/main.yml — higher-precedence variables, meant to stay more fixed, internal to the role's own logic
  • files/ — static files for a plain copy task
  • meta/main.yml — role metadata, including dependencies on other roles

Using a Role in a Playbook

- hosts: webservers roles: - nginx - firewall

Every task in every listed role runs, in order, before any play-level tasks. For finer control over ordering — interleaving a role's tasks with ordinary tasks at a specific point — include_role does the same job from inside a normal tasks: list:

- hosts: webservers tasks: - include_role: name: nginx

Ansible Galaxy — A Registry of Shared Roles

ansible-galaxy install <role> pulls a pre-built, community-maintained role from the public Galaxy registry — the same "why reinvent this" idea as tf1-7's own public Terraform Registry. ansible-galaxy init <rolename> scaffolds the entire directory structure above automatically, so the exact folder names never need to be memorized or typed by hand.

DRY Configuration — Why This Actually Matters Here

Without roles, the capstone's own real target — ws1's HTTPS/Let's Encrypt setup, UFW, fail2ban, SSH hardening, and Apache security headers — would all have to live crammed into one sprawling playbook. With roles, each of those concerns becomes its own independently testable, independently reusable unit. This directly foreshadows how ansible1-10's own capstone is actually structured: one role per ws1 concern, not one giant file.

Precedence (ansible1-5)Intended use
defaults/main.ymlLowest — meant to be overriddenSensible defaults, freely customizable by whoever uses the role
vars/main.ymlHigher — closer to fixedInternal values the role's own logic depends on, not meant for casual overriding
ansible-galaxy init scaffolds the whole tree
Running ansible-galaxy init nginx creates every subdirectory shown above, empty and ready to fill in — no need to remember the exact folder names or create them by hand one at a time.
A typo'd directory name fails silently, not loudly
Role tasks are auto-loaded purely by convention — there's no explicit "here's where the tasks live" line anywhere in a playbook. A typo like task/ instead of tasks/ doesn't raise an error; Ansible simply finds nothing there and treats the role as if it has no tasks at all, silently doing nothing. Worth checking directory spelling directly when a role appears to run successfully but does nothing.

Hands-On Exercises

Exercise 1

Sketch the directory structure (folder names only, matching this chapter's own layout) for a new role called "fail2ban" that needs its own tasks, handlers, and one default variable.

📄 View solution
Exercise 2

Explain why a role variable meant to let users customize the SSH port should live in defaults/main.yml rather than vars/main.yml, referencing this chapter's own precedence table.

📄 View solution
Exercise 3

A role named "ufw" runs successfully with no errors, but none of its intended firewall rules are ever applied. Using this chapter's own warn-box, propose the most likely explanation and how to confirm it.

📄 View solution

Chapter 7 Quick Reference

  • A role packages tasks/handlers/templates/vars/files for one concern into a standard directory structure
  • tasks/main.yml auto-loads by convention; defaults/main.yml is deliberately lowest precedence, vars/main.yml higher
  • roles: at the play level runs all listed roles first; include_role interleaves role tasks anywhere in a task list
  • Ansible Galaxy — a public registry of shared roles, the same idea as Terraform's own Registry
  • ansible-galaxy init <name> scaffolds the whole directory tree automatically
  • A typo'd directory name (task/ vs tasks/) fails silently — the role just does nothing, with no error
  • The capstone (ansible1-10) is structured as one role per ws1 concern — this chapter's own DRY payoff, made concrete