Roles & collections¶
Scope: the course page's
ansible-galaxy init roles/nginxscaffold is a single, locally-authored role used by one playbook. This page slows down on how roles get called (three different ways, with different trade-offs), what a collection is and why it sits one level above a role, and how one binary —ansible-galaxy— both scaffolds a role you're authoring and installs one someone else already published.
TL;DR — in 30 seconds:
- A role is a directory convention (
tasks/,handlers/,defaults/,vars/,meta/), not a new language — Ansible loads each subdirectory automatically by location, never by an explicitinclude:. roles:andimport_role:resolve statically at parse time — awhen:onimport_role:copies onto every task inside instead of skipping the include; onlyinclude_role:is dynamic and honorswhen:/loop:on the call itself.- A collection sits one level above a role: a namespaced (
namespace.collection) bundle of roles, modules, and plugins that installs as one unit — everyansible.builtin.*module you've called already lives inside one.
1. A role is a directory convention, not a new language¶
Nothing about a role is a new Ansible concept — it's the same tasks, handlers, templates, and variables
you already write, arranged into a fixed set of subdirectories so Ansible loads each one automatically by
its location, never by an explicit include:.
| Directory | Loaded automatically as |
|---|---|
tasks/main.yml |
The role's task list — its entry point |
handlers/main.yml |
Handlers available to anything the role notifies |
defaults/main.yml |
Lowest-precedence variables — meant to be overridden |
vars/main.yml |
Higher-precedence variables — harder for a consumer to override |
files/, templates/ |
Static files and Jinja2 templates the role's tasks reference by relative name |
meta/main.yml |
Role metadata: supported platforms, and dependencies on other roles |
A role buys you two things a loose collection of task files doesn't: portability (copy the directory
into another repo and it still works, because nothing outside it is required) and implicit wiring (no
file has to import: defaults/main.yml — the convention does it).
2. Three ways to call a role — and why it matters which one¶
- hosts: web
roles:
- role: common
- role: nginx
vars:
site_server_name: hiro.example.com
That roles: block is the form you've already seen. It is static: every listed role's tasks are
inserted before the play's own tasks: run, regardless of any condition. Two alternatives exist for when
that's not what you want:
| Form | Resolved | Can use when: / loop: on the call itself |
Typical use |
|---|---|---|---|
roles: (play-level) |
Parse time, before other tasks | No | The default — a play's fixed role composition |
import_role: |
Parse time (static) | No (the condition applies to every task inside, not the include) | Same behavior as roles:, usable mid-tasks: |
include_role: |
Run time (dynamic) | Yes | Conditionally or repeatedly including a role — e.g. one role per item in a loop: |
flowchart TB
subgraph Static["Static — roles: / import_role:"]
S1["Parsed once, before/at\nthe include's position"]
S2["Same on every run"]
end
subgraph Dynamic["Dynamic — include_role:"]
D1["Evaluated when the\nplay actually reaches it"]
D2["when: / loop: apply\nto the include itself"]
end
The gotcha this saves you from: putting a when: on an import_role: doesn't skip the include — it gets
copied onto every task inside the role, which still runs its own logic per-task. If you need "only include
this role at all when X", reach for include_role: instead.
3. Collections — the distribution unit one level above a role¶
A collection bundles roles, modules, plugins, and playbooks together under one namespaced name —
namespace.collection, for example community.general or amazon.aws. Everything you've called with a
fully-qualified name like ansible.builtin.package or ansible.posix.authorized_key is a module living
inside a collection; ansible.builtin and ansible.posix are collections just like any third-party one.
flowchart LR
C["Collection\nnamespace.collection"]
M["Modules"]
P["Plugins"]
R["Roles"]
PB["Playbooks"]
C --> M
C --> P
C --> R
C --> PB
Two reasons collections exist beyond "a bigger role":
- Distribution — a collection installs as one unit (
ansible-galaxy collection install <namespace.collection>) into a standard search path (./collections/ansible_collections/or~/.ansible/collections/), instead of you hand-copying individual role directories around. - Namespacing — the FQCN (
amazon.aws.ec2_instance) avoids the name collisions that plain module names (ec2_instance) would eventually hit once you install more than one third-party source.
4. requirements.yml — pinning what you pull in¶
A repo that depends on external roles and collections lists them once, in one file, instead of trusting whatever happens to already be installed on whoever's laptop runs the playbook:
# requirements.yml
collections:
- name: amazon.aws
version: ">=7.0.0"
- name: community.general
roles:
- name: geerlingguy.nginx
version: "3.1.0"
Two install commands, one per section — the detail newcomers usually miss, expecting one command to install both:
ansible-galaxy collection install -r requirements.yml
ansible-galaxy role install -r requirements.yml
Committing requirements.yml (and not committing the installed collections//roles/ directories it
populates) is what makes "clone the repo, install requirements, run the playbook" reproducible across
machines instead of depending on tribal knowledge of what to ansible-galaxy install by hand.
5. Role dependencies — meta/main.yml¶
A role can declare that another role must run first, every time it's used, without whoever calls it having to remember to list both:
# roles/nginx/meta/main.yml
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports: [80, 443]
flowchart LR
Firewall["firewall role"] --> Common["common role"] --> Nginx["nginx role"] --> Play["Play's own tasks"]
Dependencies run once per unique role+parameter combination per play, in the order their owning role is
listed in roles: — so a common role that three other roles all depend on runs a single time, not three.
Ansible's docs
cover allow_duplicates for the rare case you want that dependency to run every time instead.
6. Reuse in practice — where a role should come from¶
| Source | Get it with | Fits when |
|---|---|---|
| Local, in-repo | Write it directly under roles/ |
Logic specific to your environment — the nginx role in the course page |
| Ansible Galaxy (community) | ansible-galaxy role install <author.role> or a requirements.yml entry |
A well-trodden need (an nginx role, a Postgres role) someone's already hardened |
| A collection | ansible-galaxy collection install <namespace.collection> |
You need the modules (e.g. amazon.aws.ec2_instance), which may also ship roles and plugins alongside them |
ansible-galaxy init roles/<name> scaffolds the directory skeleton for the local-authoring path; the same
tool's collection subcommands (ansible-galaxy collection init, collection install) handle the other
two. One binary, two different jobs depending on whether you're authoring or consuming.
7. How this connects¶
The course page's single roles/nginx is the local-authoring path in its simplest form: no dependencies,
no external collection, called statically from roles:. This page is what to reach for once you need a
role to pull in another role automatically (meta/main.yml), need modules that don't ship with
ansible-builtin (a collection), or need the same external role pinned to a version across every clone of
the repo (requirements.yml).
Common gotchas
- Putting
when:on animport_role:expecting it to skip the whole role. Fix: it gets copied onto every task inside instead, which still runs its own per-task logic — reach forinclude_role:when you need "only include this role at all under condition X." - Writing a tiny, single-use role "for reuse" with no second caller in sight. Fix: a role under ~30 lines used exactly once is often premature abstraction — the indirection cost isn't paid back yet.
- Pointing a Git-sourced role/collection at a branch (
?ref=mainor no ref). Fix: a branch ref means every install can silently pull in whatever's merged since your last run — pin an immutable tag or version instead. - Expecting
ansible-galaxy install -r requirements.ymlto install both roles and collections in one command. Fix: it's two separate commands —ansible-galaxy collection install -r requirements.ymlandansible-galaxy role install -r requirements.yml— one per section.
Check yourself — you add a when: condition to an import_role: call, expecting the whole role to be skipped when the condition is false. What actually happens?
The condition doesn't skip the include — it's applied to every individual task inside the role, which
still evaluates its own logic per-task at parse time. To conditionally include a role at all, use
include_role: instead, which resolves dynamically and honors when: on the call itself.
Check yourself — your repo's requirements.yml lists both collections: and roles:. You run ansible-galaxy collection install -r requirements.yml and the playbook still can't find geerlingguy.nginx. Why?
Collections and roles install with two separate commands — ansible-galaxy collection install -r
requirements.yml only installs the collections: section; the roles: section needs its own
ansible-galaxy role install -r requirements.yml run.
You can defend this when you can say why import_role: and include_role: behave differently under a
when: condition, explain what problem a collection solves that a role alone doesn't, and describe what
requirements.yml actually reproduces (pinned external roles/collections) versus what it doesn't
(your own in-repo roles, which are already in the clone).