Choosing the Right Unit of Reuse
Scenario: A repository contains a role called stuff with 340 tasks, and eleven near-copies of a four-line task file. Nobody wants to touch either.
New words, in plain English
- Cohesion - how strongly the parts of a unit belong together.
- Coupling - how much one unit depends on the internals of another.
- Single responsibility - a unit does one thing, and its name says what.
- Composition - building larger behaviour by combining small units.
Ansible gives you four units of reuse, and the skill is picking the right one rather than defaulting to whichever you used last.
A task file (import_tasks) is right for a handful of steps used in two or three places within one role or playbook. It is the cheapest option and carries no interface.
A role is right for a coherent capability with its own variables, handlers, templates and defaults - "configure nginx", "harden SSH". A role is the unit teams share, and the one worth writing a README for.
A collection is right when you have several related roles plus custom modules or plugins, and you want to version and distribute them together.
A playbook composes the above for a specific outcome, and should stay thin.
Two failure modes bracket the sensible middle. A role named stuff or common that accumulates everything nobody could place elsewhere becomes untestable and un-deletable. Eleven near-identical four-line task files are the opposite failure: duplication that drifts, so a fix lands in ten of them.
The honest test for a role is whether you can state, in one sentence, what it needs, what it does and what it guarantees afterwards. If that sentence needs an "and" joining unrelated things, it should be two roles.
Analogy: A task file is a paragraph, a role is a chapter, a collection is a book, a playbook is the reading list for a course. A 340-task role is a chapter with no title that nobody can cite; eleven duplicated task files are the same paragraph retyped in eleven chapters, drifting apart with every edit.
A worked example
# A repository that has chosen its boundaries deliberately
#
# ansible-project/
# ├── requirements.yml # pinned external collections and roles
# ├── site.yml # thin: imports per-tier playbooks
# ├── playbooks/
# │ ├── webservers.yml # roles: [common, webapp]
# │ └── dbservers.yml # roles: [common, postgres]
# └── roles/
# ├── common/ # ONE thing: baseline every host needs
# │ └── tasks/main.yml
# ├── webapp/ # ONE thing: the checkout application
# │ ├── defaults/main.yml
# │ ├── tasks/main.yml
# │ ├── handlers/main.yml
# │ └── templates/webapp.conf.j2
# └── postgres/ # ONE thing: the database service
#
# Test for each role: "it needs X, it does Y, afterwards Z is true."
Prefer a well-maintained community role to writing your own for genuinely standard software - but read it first. A role you do not understand is a dependency you cannot debug at 3 AM, and popularity on Galaxy is not the same as suitability for your estate. Pin it, and treat an upgrade like any other dependency change.
When a role does grow too large, the natural seams are usually visible in its own task file: groups of tasks separated by comments, sharing a when, or touching a distinct part of the system. Those comment headings are almost always the role boundaries you should have had.
Tip: If you cannot name a role without using the word 'and', or without using a word like 'stuff', 'misc' or 'common-2', you have found a boundary problem rather than a naming problem.
Goal: Put this to work in the ansible-dynamic-inventory lab. Open/labs/ansible, pickansible-dynamic-inventory, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.