Skip to main content
RunBook Academy

AnsibleXXII · Roles and ReuseRoles and reuse

Dependencies and the roles you did not ask for

Intermediate⏱ ~20 minansible-playbook

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

Not yet marked complete on this device.

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
Read-only / Safethe dependency runs first, and inherits the tags
$ ansible-playbook -i inventory.ini rolesdep.yml --list-tasks
playbook: 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:

  • gamma runs before beta. Dependencies are prerequisites, so this is the intended semantic and it is the reason to use the feature.
  • gamma inherited beta’s tag. --tags deploy now 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:

Read-only / Safewhat the parser reports
$ ansible-playbook -i inventory.ini deps.yml --list-tasks
playbook: 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: []
Read-only / Safewhat actually runs
$ ansible-playbook -i inventory.ini deps.yml
TASK [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:

Read-only / Safelisted twice explicitly, runs once
$ ansible-playbook -i inventory.ini dup.yml
TASK [gamma : gamma task one] **************************************************

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

It runs twice when the parameters differ

Read-only / Safesame role, different role parameters
$ ansible-playbook -i inventory.ini dup3.yml
TASK [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=0
roles:
  - 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

Read-only / Safeallow_duplicates: true
$ ansible-playbook -i inventory.ini dup2.yml
TASK [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

  1. Q1. A play lists one role, beta, tagged deploy. beta declares gamma as a meta dependency. What does --tags deploy execute?

  2. Q2. Because --list-tasks is generated by the parser, its output is an exact preview of the tasks a run will execute.

  3. Q3. Under which conditions does a role listed more than once in one play actually execute more than once? Select all that apply.

  4. 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.