AnsibleXXII · Roles and ReuseRoles and reuse
Dependencies and the roles you did not ask for
What you'll learn
- Predict the execution order a meta dependency produces
- State when a role listed twice runs twice and when it runs once
- Use allow_duplicates deliberately rather than discovering it
- Recognise that --list-tasks over-reports duplicate roles
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
Lesson 2 argued that a role which assumes a sibling ran first has an unstated
dependency. meta/main.yml is the mechanism for stating it:
# roles/beta/meta/main.yml
dependencies:
- role: gamma
Ansible always executes roles listed in dependencies before the role
that lists them. That is the useful half. The other half is that this
happens invisibly from the playbook — a play listing one role can run four,
in an order the file does not show.
Ordering, executed
A play that lists beta and nothing else, where beta depends on gamma:
- name: roles keyword with a meta dependency
hosts: web
gather_facts: false
roles:
- role: beta
tags: [deploy]
tasks:
- name: a play task
ansible.builtin.debug:
msg: play-task
$ ansible-playbook -i inventory.ini rolesdep.yml --list-tasksplaybook: rolesdep.yml
play #1 (web): roles keyword with a meta dependency TAGS: []
tasks:
gamma : gamma task one TAGS: [deploy]
beta : beta task one TAGS: [deploy]
a play task TAGS: []Two facts, both consequential:
gammaruns beforebeta. Dependencies are prerequisites, so this is the intended semantic and it is the reason to use the feature.gammainheritedbeta’s tag.--tags deploynow runs a role the operator did not know was in scope. Tag inheritance flows through the dependency chain exactly as it flows through a static import.
Dependencies are transitive. A role that depends on a role that depends on
two more produces a chain the play never mentions, and the only way to see it
is --list-tasks — which brings its own caveat, below.
Deduplication, and where the dry run lies
A role listed twice in the same play normally runs once. That rule has three exceptions, and one of them is not what people expect.
A role reached through two dependency paths runs once
Two roles, dep1 and dep2, both declaring gamma as a dependency:
$ ansible-playbook -i inventory.ini deps.yml --list-tasksplaybook: deps.yml
play #1 (local): local TAGS: []
tasks:
gamma : gamma task one TAGS: []
dep1 : dep1 body TAGS: []
gamma : gamma task one TAGS: []
dep2 : dep2 body TAGS: []$ ansible-playbook -i inventory.ini deps.ymlTASK [gamma : gamma task one] **************************************************
TASK [dep1 : dep1 body] ********************************************************
TASK [dep2 : dep2 body] ********************************************************
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0--list-tasks over-reports duplicate roles. It shows the tree the parser
built; deduplication happens when the run reaches the second copy. Lesson 5
established that the dry run under-reports dynamic includes. This is the
same tool erring in the opposite direction for a different reason, and both
are worth knowing before you use --list-tasks output as evidence in a
change review.
The same applies to a role written out twice by hand:
$ ansible-playbook -i inventory.ini dup.ymlTASK [gamma : gamma task one] **************************************************
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0It runs twice when the parameters differ
$ ansible-playbook -i inventory.ini dup3.ymlTASK [gamma : gamma task one] **************************************************
TASK [gamma : gamma task one] **************************************************
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0roles:
- role: gamma
some_param: one
- role: gamma
some_param: two
This is the sane behaviour — a role parameterised two ways is doing two
different jobs — and it is also how a dependency chain can execute a role
several times without anybody intending it. Two roles that both depend on
gamma but pass it different parameters get two runs of gamma, not one.
It runs twice when the role says so
$ ansible-playbook -i inventory.ini dup2.ymlTASK [dupok : dupok runs] ******************************************************
TASK [dupok : dupok runs] ******************************************************
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0# roles/dupok/meta/main.yml
allow_duplicates: true
Set it when the role is genuinely an operation rather than a state — “deploy one instance of the app”, called once per instance. Leave it alone otherwise, because a role that describes a desired state has nothing to gain from converging twice and everything to lose if it is not idempotent.
When a dependency is the wrong tool
Dependencies are seductive because they make a role self-sufficient with one file. They have three costs that accumulate quietly.
The execution order stops being readable. A five-line play whose roles
each carry two dependencies runs a tree. The playbook is no longer a
description of what happens; --list-tasks is, and only approximately.
Variables flow in a direction people do not expect. A dependency is
invoked from within the depending role’s context, so parameters passed to
beta are not automatically passed to gamma. Values gamma needs must be
set where gamma can see them, and “where gamma can see them” is a
question about precedence rather than about the dependency graph.
Removing one is a breaking change to every consumer, and the breakage is of the quiet kind from lesson 2: a role that used to arrive with its prerequisites now arrives without them.
The alternative that costs less is usually explicit composition — list the roles in the play, in order, where a reader can see them:
- name: build the web tier
hosts: web
roles:
- baseline
- webapp
- monitoring_agent
Three lines, one order, no hidden tree. Reserve meta/main.yml dependencies
for the case they are genuinely good at: a role published for other people,
whose prerequisites are an implementation detail its callers should not have
to know.
Knowledge check
Knowledge check · 4 questions
Q1. A play lists one role, beta, tagged deploy. beta declares gamma as a meta dependency. What does --tags deploy execute?
Q2. Because --list-tasks is generated by the parser, its output is an exact preview of the tasks a run will execute.
Q3. Under which conditions does a role listed more than once in one play actually execute more than once? Select all that apply.
Q4. When is a meta/main.yml dependency the better choice over listing both roles explicitly in the play?
Passing score: 75%. Answers are checked in this browser.