AnsibleXXII · Roles and ReuseRoles and reuse
What belongs in one role
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
“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”.
webapp — configures and runs the storefront application. Fine; the “and”
there is one action described two ways.
common — installs 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 large | Too small |
|---|---|
One common role configuring the whole estate | Two hundred roles, one task each |
Cannot be run partially, so --tags grows to compensate | Every playbook is fifty role lines with an ordering nobody can verify |
| Change to timezone handling requires reviewing SSH hardening | Interfaces 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
ansible-playbook -i inventory/scratch.ini standalone.yml --list-tasksansible-playbook -i inventory/scratch.ini standalone.ymlThen 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
Q1. A team reviews a role and asks whether it is the right size. Which question gives the most reliable answer?
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.
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.
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.