skip to content

What does the dependencies list in an Ansible role's meta/main.yml do, and how does Ansible handle the same dependency being pulled in by several roles?

level: seniorimportance: nice to knowfreq 32%

answer

  1. runs before the role that declares it
  2. flattened depth-first, statically
  3. run count is not the dependent count
  4. same parameters means run once
  5. allow_duplicates lives in the dependency

basics

~20 s

The dependencies list in a role's meta/main.yml names roles Ansible applies before the role itself. Within one play a dependency runs only once for a given set of parameters, unless its own meta/main.yml sets allow_duplicates: true.

solid answer

~50 s

`dependencies:` in `meta/main.yml` declares roles that must run first. They are applied statically, in listed order, before the depending role's own tasks, and their own dependencies are resolved the same way, so the graph is flattened depth-first. The de-duplication rule is the part people get wrong: within a play, a role that appears more than once runs only once - unless the parameters differ between invocations, in which case it runs again. A role that must run every time it is pulled in has to set `allow_duplicates: true` in its own `meta/main.yml`. That is why a `common` role listed by five other roles bootstraps once, while a `deploy-app` role that expects to run per application needs the flag. In practice many teams avoid meta dependencies entirely and call roles explicitly with `import_role`, because dependency ordering is invisible at the call site.

code

yaml · 10 lines
yaml
# roles/webserver/meta/main.yml
dependencies:
  - role: common
  - role: firewall
    firewall_open_ports: [80, 443]

# roles/deploy-app/meta/main.yml
# needed when the role is meant to run once per invocation
allow_duplicates: true
dependencies: []

go deeper

for a junior

Know that meta/main.yml can list other roles under dependencies and that those roles run first. You are not expected to know the de-duplication rule at this level.

for a middle

Explain that dependencies are static, resolved recursively and flattened before the role's own tasks, and that a role is applied once per play unless its parameters differ.

for a senior

Own the run-count question end to end, including allow_duplicates living in the dependency's own metadata, and be able to argue when explicit import_role composition beats dependency metadata in a large repository.

for a principal

Decide the composition standard for the estate: whether shared roles may declare dependencies at all, how bootstrap ordering is expressed so a reviewer can see it, and how you stop role graphs growing into an unpredictable tree.

## What the list does ```yaml # roles/webserver/meta/main.yml dependencies: - role: common - role: firewall firewall_open_ports: [80, 443] ``` When `webserver` is applied, Ansible first applies `common`, then `firewall` (with that parameter), then runs `webserver/tasks/main.yml`. Dependencies are resolved recursively - a dependency's own dependencies run before it - and the whole thing is flattened into the play depth-first, ahead of the depending role. Dependencies behave like static imports: they are resolved at parse time, they cannot be looped, and their tasks are visible in `--list-tasks`. ## De-duplication is the interview point Within a single play, Ansible applies a given role **once**, even if several roles list it as a dependency or the play names it repeatedly. This is what makes a `common` bootstrap role safe to depend on from everywhere: five dependents do not mean five runs. The rule has one exception built in and one escape hatch: 1. **Different parameters count as a different invocation.** If `webserver` depends on `firewall` with `firewall_open_ports: [80, 443]` and `dbserver` depends on `firewall` with `firewall_open_ports: [5432]`, `firewall` runs twice, because the parameter sets differ. 2. **`allow_duplicates: true`** in the *dependency's own* `meta/main.yml` disables de-duplication for that role, so it runs on every invocation regardless of parameters. ```yaml # roles/deploy-app/meta/main.yml allow_duplicates: true ``` Getting this backwards produces two mirror-image bugs. A role designed to be applied once per application, applied from a loop or from several parents without the flag, silently runs a single time and only the last-configured app exists. Conversely a heavyweight bootstrap role with `allow_duplicates: true` on it re-runs for every dependent and turns a two-minute play into a twenty-minute one. ## The other thing in meta/main.yml The same file carries `galaxy_info` - author, description, `license`, `min_ansible_version`, the `platforms` list, and `galaxy_tags`. This is metadata for humans and for Galaxy; it does not affect execution, other than `min_ansible_version` being informational rather than enforced at runtime. Do not conflate declaring a supported platform with checking one; if the role genuinely cannot run on a distribution, assert that in `tasks/main.yml`. ## Why many teams stop using dependencies Meta dependencies were the original composition mechanism, and they still work, but they have real ergonomic problems in a large repository: - **Invisible ordering.** Reading a playbook tells you nothing about the fifteen roles that will run first; you have to open every role's `meta/main.yml` to reconstruct the order. - **Parameters at a distance.** Configuring a dependency means editing the depending role's metadata, not the playbook, so environment-specific values leak into shared role code. - **Surprising de-duplication.** The "same params or it runs again" rule is subtle enough that most engineers cannot predict the run count of a moderately deep graph. - **No conditionality.** A dependency cannot be skipped with `when` at the call site the way an `import_role` in `tasks:` can. The common modern alternative is to keep roles dependency-free and compose them explicitly in the playbook, or from the top of `tasks/main.yml` with `import_role`. Ordering then lives where the reader is looking, and each application is unconditionally at the point written. Some teams keep meta dependencies only for genuinely universal bootstrap roles, where run-once semantics are exactly what they want. ## What to say in an interview Describe the mechanism, then the de-duplication rule with the parameter caveat and `allow_duplicates`, then take a position: dependencies are fine for a single idempotent bootstrap role, and explicit composition in the playbook is clearer for everything else. A candidate who only knows that `dependencies:` exists has read the docs; one who can explain why a role ran once when they expected five times has actually shipped with them.

  • Five roles in one play each depend on common. How many times does common run, and why?
    Once, assuming all five pull it in with the same parameters - Ansible de-duplicates a role within a play. If one of them passed different parameters, that invocation counts as distinct and `common` runs again for it. If `common` itself sets `allow_duplicates: true`, de-duplication is off and it runs five times.
  • Why do many teams prefer explicit import_role over meta dependencies?
    Ordering and parameters become visible at the call site instead of buried in each role's metadata, the role can be made conditional with `when`, and there is no de-duplication rule to reason about. Dependencies are still reasonable for one universal, idempotent bootstrap role where run-once semantics are what you actually want.
  • Can a meta dependency be applied conditionally or in a loop?
    No. Dependencies behave like static imports resolved at parse time - there is no `when` and no `loop` on a dependency entry. If you need either, drop the dependency and call the role from `tasks:` with `import_role` (for a condition) or `include_role` (for a loop or a runtime-chosen name).

saying these in an interview costs you the question

  • Says a dependency runs once per dependent role
  • Thinks allow_duplicates goes in the calling role
  • Believes dependencies run after the role's own tasks
  • Claims dependencies accept when or loop
  • Treats min_ansible_version as an enforced runtime check

context