Skip to main content
RunBook Academy

AnsibleXXII · Roles and ReuseRoles and reuse

What belongs in one role

Intermediate⏱ ~18 minansible-playbook

What you'll learn

  • Apply the standalone test to decide whether a set of tasks is one role or two
  • Size a role using a stated heuristic rather than taste
  • Recognise the implicit-dependency smell before it becomes an incident
  • Name a role in one sentence and use failure to do so as a signal

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

“How big should a role be?” gets answered with platitudes — single responsibility, do one thing well — that are true and unusable. Two engineers who both believe in single responsibility will draw the boundary in different places and both feel principled about it.

There is a test that is not a matter of taste, because it is a question about behaviour rather than about design:

Can this role run, on its own, against a host that has nothing else of yours on it, and leave that host in the state the role claims?

If yes, it is a role. If no, it is a fragment of a role that happens to live in its own directory, and the missing piece is a dependency that exists only in somebody’s head.

Why the test is operational, not aesthetic

The reason this matters is not tidiness. It is that a role which assumes a sibling ran first has a blast radius you cannot state.

Consider a webapp role whose tasks write /etc/webapp/app.conf and start the service. It works, has worked for two years, and is deployed by a play that also runs baseline first. baseline creates the webapp system user, creates /etc/webapp with mode 0750, and installs the package.

Now somebody needs the app on one new host and writes the obvious thing:

- name: bring up the new node
  hosts: web-07.example.com
  roles:
    - webapp

What happens is not a clean failure. ansible.builtin.template creates /etc/webapp/app.conf — the directory does not exist, so the task fails, and that part is fine. But swap the ordering slightly, or have file: create the directory with the role’s own default owner, and you get a config file owned by root:root in a directory the service cannot read, a service that starts and immediately exits, and a play that reported ok. The diagnosis goes to the application team because the automation said it succeeded.

The role was never wrong. It was never complete, and nothing in the repository said so.

The naming heuristic

The sizing question has a shortcut that works better than it has any right to:

Name the role in one sentence, without using the word “and”.

webappconfigures and runs the storefront application. Fine; the “and” there is one action described two ways.

commoninstalls the monitoring agent and sets the timezone and configures sudo and hardens SSH and installs the base package set. That is five roles wearing one name, and the tell is that no sentence about it can avoid the conjunction.

webapp_install_step_three — you cannot name it at all without referring to its neighbours, which means it is not a unit; it is a line in a procedure.

The heuristic works because a role’s name is the only thing most operators ever read about it. A role you cannot name in a sentence is a role whose effect nobody can predict from the playbook, and the playbook is where people look at 03:00.

Both failure directions are real

Too largeToo small
One common role configuring the whole estateTwo hundred roles, one task each
Cannot be run partially, so --tags grows to compensateEvery playbook is fifty role lines with an ordering nobody can verify
Change to timezone handling requires reviewing SSH hardeningInterfaces outnumber implementation; each role has a defaults/ file with one entry
Blast radius is “everything, always”Blast radius is stated per role and unknowable in aggregate

The microscopic-role failure is less discussed and equally real. A play with fifty roles has fifty implicit ordering constraints, and the only artefact recording them is the order of the lines. Delete one, reorder two, and the run still succeeds — until it does not.

A workable middle: a role should correspond to a thing an operator would name in an incident. “The nginx role.” “The postgres role.” “The node-exporter role.” If you would not say the name out loud in a sentence about production, the boundary is probably in the wrong place.

Testing the boundary rather than asserting it

The test is executable. Take the role, a minimal inventory pointing at a disposable host with nothing else on it, and a play that runs only that role:

# standalone.yml - the boundary test as a file
- name: does webapp stand alone
  hosts: scratch
  roles:
    - webapp
Read-only / Safewhat will this role do, before running it
ansible-playbook -i inventory/scratch.ini standalone.yml --list-tasks
Service impact possiblethe real standalone run
ansible-playbook -i inventory/scratch.ini standalone.yml

Then run it a second time. A role that stands alone converges on the first run and reports zero changes on the second. A role that depends on a sibling usually fails on the first run, which is the good outcome — the bad outcome is the one that reports ok while leaving the host subtly wrong.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A team reviews a role and asks whether it is the right size. Which question gives the most reliable answer?

  2. Q2. A role that silently assumes another role ran first has a blast radius nobody can state, because the failure it produces may be a play that reports success while leaving the host wrong.

  3. Q3. Which of these are machine-readable statements of what a role requires, in the sense that they fail the run when unmet? Select all that apply.

  4. Q4. What is the specific operational cost of splitting an estate into two hundred single-task roles?

Passing score: 75%. Answers are checked in this browser.