Lesson 7 of 9 · 22m
Roles and Collections: The Units of Reuse
The role directory contract, defaults vs vars, dependencies and meta, and how collections package content — plus when a role earns its own repo.
A playbook that does everything is a bash script with YAML syntax — we've all written one. Roles are how Ansible actually scales: a role is a directory with a contract — put tasks in tasks/, defaults in defaults/, handlers in handlers/ — and any playbook can consume it by name. Once you've written "install postgres" as a role, you never write it again.
The directory contract
roles/
postgres/
tasks/main.yml # the work — entry point, always
handlers/main.yml # restart/reload logic
defaults/main.yml # overridable knobs: postgres_version: 15
vars/main.yml # internal constants: repo URL, service name
files/ # static files for copy:
templates/ # .j2 files for template:
meta/main.yml # dependencies, galaxy_info
molecule/ # verification lives hereReference it in a play and the pieces wire themselves:
- hosts: dbservers
roles:
- role: postgres
vars:
postgres_version: 16
postgres_max_connections: 400defaults vs vars: the contract boundary
defaults/main.yml sits at the bottom of precedence — every value there is a public API the caller may override. vars/main.yml sits near the top — it's for things callers must not change (the package name, the service unit name). Getting this backwards is the most common role bug: a "configurable" port in vars/ silently wins over the operator's group_vars, and the port never actually changes.
meta/main.yml: dependencies and metadata
meta/main.yml holds galaxy_info and dependencies: — dependent roles run before this one (postgres needing common first is a real pattern), and a role runs only once per play with the same parameters, so diamonds in the dependency graph don't re-install the world.
Collections: roles and modules as versioned packages
A collection is a namespaced package — namespace.collection — holding roles, modules, and plugins versioned together. That's why modern playbooks use FQCNs: community.general.ufw, amazon.aws.ec2_instance, ansible.posix.firewalld.
# requirements.yml — install with: ansible-galaxy collection install -r requirements.yml
collections:
- name: community.general
version: "8.5.0"
- name: amazon.aws
version: ">=7.0.0,<9.0.0"Pin versions — community.general floating to latest is how a routine deploy starts failing on a module rename.
Field notes
- A role should be independently testable: if you can't run it against a bare VM and converge, it has hidden coupling.
roles/in the project beats globalansible-galaxyinstalls for team-shared code — same repo, same review, same checkout.- Import order:
roles:run afterpre_tasks:and beforetasks:/post_tasks:.import_roleis static;include_roleis dynamic (can be looped, conditioned).