Skip to content

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 here

Reference it in a play and the pieces wire themselves:

- hosts: dbservers
  roles:
    - role: postgres
      vars:
        postgres_version: 16
        postgres_max_connections: 400

Playbook loading a role's directories

defaults 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 global ansible-galaxy installs for team-shared code — same repo, same review, same checkout.
  • Import order: roles: run after pre_tasks: and before tasks:/post_tasks:. import_role is static; include_role is dynamic (can be looped, conditioned).