Dependency #

In large-scale automation architecture development, it’s very rare for a role to stand alone in isolation. A role tasked with deploying your Node.js web application, for example, almost certainly needs the Node.js runtime installed first. Likewise, a PostgreSQL database role needs the base OS security configuration and storage directories prepared by a base role.

If you don’t have a mechanism to manage these inter-role relationships, you’re forced to remember and manually write those role lists in the correct order in every playbook you create. This is very impractical and prone to human error. To solve this problem, Ansible provides a dependency management feature that lets you declare relationships between roles declaratively and automatically.


Declaring Dependencies Through meta/main.yml #

In Ansible, role dependencies are declared exclusively in the meta/main.yml file located in the relevant role directory. You use the dependencies key to define the list of other roles that must execute before your main role runs.

Let’s look at the basic dependency declaration structure on an application role named myapp:

# roles/myapp/meta/main.yml
# CORRECT: Declaring other role dependencies with supporting variables
---
galaxy_info:
  author: "Infrastructure Team"
  description: "Role for deploying our internal application"
  company: "Example Corp"
  license: "MIT"
  min_ansible_version: "2.10"
  platforms:
    - name: Ubuntu
      versions:
        - "22.04"

# List of roles that must run BEFORE the myapp role runs
dependencies:
  - role: common
  - role: nodejs
  - role: postgresql

When you call the myapp role in your playbook, Ansible automatically detects this dependencies list. Ansible then downloads (if not yet present) and executes the common role, followed by nodejs, then postgresql, before finally running the tasks inside the myapp role.

Passing Variables to Dependencies #

One of the most powerful features of Ansible’s dependency system is the ability to pass variables to the dependency roles. This lets you customize the dependency roles’ behavior to match your main role’s specific needs.

Let’s look at a complex variable-passing example in meta/main.yml:

# roles/myapp/meta/main.yml
---
dependencies:
  - role: common
    vars:
      common_enable_firewall: true
      common_allowed_ports:
        - 80
        - 443

  - role: nodejs
    vars:
      nodejs_version: "18.x"
      nodejs_install_global_packages:
        - pm2
        - yarn

  - role: postgresql
    vars:
      postgresql_databases:
        - name: "myapp_prod"
          owner: "myapp_user"
      postgresql_users:
        - name: "myapp_user"
          password: "{{ vault_db_password }}"

With this method, the generic nodejs and postgresql roles can be dynamically customized specifically for the myapp deployment needs. The variables passed through this dependency system have an isolated scope for that dependency’s execution, so they won’t accidentally break the configuration of the same role in other parts of the play.


Recursive Dependency Resolution Mechanism and Order #

Ansible resolves role dependencies using a recursive depth-first search algorithm. This means if Role A depends on Role B, and Role B itself has a dependency on Role C, then Ansible resolves the entire dependency chain down to the deepest end before executing Role A.

Let’s visualize this recursive dependency resolution chain:

Playbook calls Role A
     │
     ├── Evaluate Role A's dependencies -> Finds Role B
     │        │
     │        └── Evaluate Role B's dependencies -> Finds Role C
     │                 │
     │                 ▼
     │            [Step 1] Run Role C (Deepest dependency end)
     │                 │
     │                 ▼
     │            [Step 2] Run Role B (C's dependencies done)
     │                 │
     │                 ▼
     │            [Step 3] Run Role A (B's dependencies done)

For a more structured visual understanding of this dependency resolution flow, let’s look at the following flowchart:

flowchart TD
    Start["Playbook calls the Main Role (A)"] --> CheckDep{"Does Role A have dependencies in meta/main.yml?"}
    CheckDep -- "Yes" --> GetDep["Take the dependency list (e.g. B)"]
    GetDep --> CheckDepB{"Does Role B have dependencies?"}
    CheckDepB -- "Yes" --> GetDepB["Take the dependency list (e.g. C)"]
    GetDepB --> RunC["Run the tasks inside Role C"]
    RunC --> RunB["Run the tasks inside Role B"]
    CheckDepB -- "No" --> RunB
    RunB --> RunA["Run the tasks inside Role A"]
    CheckDep -- "No" --> RunA
    RunA --> End["Done: The entire role execution flow is satisfied"]

Dependency Position in the Playbook Lifecycle #

It’s important to understand the execution position of role dependencies inside the Ansible playbook execution lifecycle. The process order within a play is defined as follows:

  1. pre_tasks: Tasks defined in this section execute first, before anything else.
  2. Handlers from pre_tasks: If any handlers are triggered by pre_tasks, they run here.
  3. Role Dependencies (dependencies): Ansible executes all role dependencies declared in meta/main.yml in order.
  4. Main Roles (roles): After dependencies finish, the main role’s tasks execute.
  5. tasks: Regular tasks written directly in the playbook.
  6. Handlers from roles and tasks: All handlers triggered during the role and task execution phases run here.
  7. post_tasks: Tasks defined to run last after the entire main process finishes.
  8. Handlers from post_tasks: Executed at the very end of the cycle.

Understanding this order is crucial to prevent bugs where a main playbook task assumes something was already configured by a role, when in execution cycle terms it hasn’t run yet.


Controlling Execution Duplication with allow_duplicates #

By default, Ansible applies a deduplication mechanism to roles. If several roles depend on the same role, Ansible only executes that dependency role once. This is a very logical and safe built-in behavior.

For example, imagine a Playbook with this structure:

  • The webserver role depends on the common role.
  • The database role also depends on the common role.

Without deduplication, the common role (which might do apt package updates and firewall configuration) would run twice wastefully. Deduplication ensures your servers don’t do the same work repeatedly, significantly saving playbook execution time.

Using allow_duplicates: true #

However, there are times when you actually want to run the same role several times with different parameters. For example, you have a role named vhost tasked with creating Nginx virtual host configurations. You want to call this vhost role several times for different domains on the same server.

To disable this built-in deduplication feature, you must add the allow_duplicates: true parameter inside the meta/main.yml file of the role you want to run repeatedly.

Let’s look at the implementation code difference:

# roles/vhost/meta/main.yml
# CORRECT: Allows this vhost role to run multiple times in one playbook
---
allow_duplicates: true
dependencies: []

Now, you can call this vhost role several times as a dependency in the main role or directly in your playbook without being blocked by Ansible’s deduplication system:

# playbook.yml
# CORRECT: Calling the vhost role multiple times for different domains
- name: Multi Domain Nginx Setup
  hosts: webservers
  roles:
    - role: vhost
      vars:
        vhost_domain: "blog.ourcompany.com"
        vhost_root: "/var/www/blog"

    - role: vhost
      vars:
        vhost_domain: "shop.ourcompany.com"
        vhost_root: "/var/www/shop"

To clarify how Ansible decides whether to execute a dependency role or skip it due to deduplication, let’s study the following decision logic diagram:

flowchart TD
    Start["Ansible processes the Dependency Role call"] --> CheckRun{"Has this role already been executed earlier in this play?"}
    CheckRun -- "No" --> Execute["Execute the Dependency Role"]
    CheckRun -- "Yes" --> CheckDup{"Is 'allow_duplicates: true' declared in that role's meta/main.yml?"}
    CheckDup -- "Yes" --> Execute
    CheckDup -- "No" --> Skip["Deduplication Active: Skip the role execution to save time"]

Managing External Dependencies Using requirements.yml #

When you work in a team or manage complex infrastructure, you often need to use community-made roles (like from Ansible Galaxy) or roles shared between internal teams through company Git repositories (GitLab/GitHub).

Copying third-party role source code directly into your project’s Git roles/ directory is a bad anti-pattern. It causes:

  • Code Bloat: Your Git repository becomes very large because it stores third-party code that isn’t yours.
  • Upgrade Difficulty: It’s very hard to update those roles to the latest version when the original author releases bug fixes or new features.
  • Lost History: You lose the version tracking of the original role.

The Solution: requirements.yml #

The best solution to this problem is using an external dependency declaration file named requirements.yml. This file acts like package.json in Node.js or requirements.txt in Python. You write the list of external roles you need along with their versions in this file.

Let’s look at a comprehensive requirements.yml file example:

# requirements.yml
# CORRECT: Documenting external dependencies from Galaxy and Git with pinned versions
---
roles:
  # 1. Downloading roles from the official Ansible Galaxy
  - name: geerlingguy.nginx
    version: "3.2.0"

  - name: geerlingguy.postgresql
    version: "3.4.0"

  # 2. Downloading roles from our company's internal Git repository
  - name: common-security
    src: "[email protected]:badricreativetech/ansible-role-common-security.git"
    scm: git
    version: "v1.5.2"

  # 3. Downloading roles from GitLab using an authentication token
  - name: db-backup
    src: "https://gitlab-ci-token:{{ lookup('env', 'GITLAB_TOKEN') }}@gitlab.ourcompany.com/infra/role-db-backup.git"
    scm: git
    version: "main"

collections:
  # We can also define Ansible collection dependencies here
  - name: community.general
    version: "8.1.0"
  - name: amazon.aws
    version: "6.5.0"

How to Install Dependencies #

After writing the requirements.yml file at your project root, you can instruct Ansible to download all those dependencies before running your main playbook.

Use the following terminal command to install all listed roles and collections:

# Install all roles and collections at once to the default directory
ansible-galaxy install -r requirements.yml

If you want to isolate role installation into a local directory inside your project (so it doesn’t mix with global system roles), you can use the --roles-path option:

# Install roles directly into our project's local roles subdirectory
ansible-galaxy role install -r requirements.yml --roles-path ./roles

[!IMPORTANT] Always Do Version Pinning
It’s crucial to always specify the version parameter specifically in requirements.yml (for example "3.2.0" or the Git tag "v1.5.2"). Avoid using dynamic branches like master or main except for development purposes. If you don’t pin the version, automatic updates from external role authors can accidentally break (breaking changes) your production configuration when you redeploy in the future.


Best Practice vs Anti-Pattern in Dependency Management #

To make sure your automation system stays stable, maintainable, and efficient, here’s a comprehensive comparison table of what to do (best practice) and what to avoid (anti-pattern) when managing role dependencies in Ansible:

Real Case Study #

Let’s study a case example that often breaks Ansible repositories in enterprise environments:

# roles/roleA/meta/main.yml
# ANTI-PATTERN: Writing a circular dependency that makes Ansible hit a fatal error
---
dependencies:
  - role: roleB
# roles/roleB/meta/main.yml
# ANTI-PATTERN: roleB calls roleA back in a circle
---
dependencies:
  - role: roleA

When you try to run a playbook calling one of the roles above, Ansible gets stuck in an endless dependency search loop until it finally spits out an error message: [ERROR]: CIRCULAR DETECTED: Circular dependency detected between roleA and roleB.

The Correct Solution:
You must analyze which tasks roleA and roleB actually need shared. Move those shared tasks into a third neutral role (for example role_common), then make roleA and roleB depend on role_common linearly without calling each other.

# roles/roleA/meta/main.yml
# CORRECT: One-way linear dependency to a base role
---
dependencies:
  - role: role_common
# roles/roleB/meta/main.yml
# CORRECT: One-way linear dependency to a base role
---
dependencies:
  - role: role_common

By keeping the dependency direction one-way and non-circular, you guarantee playbook execution stability and avoid confusing compilation errors.


Summary #

  • meta/main.yml is the central declaration file for defining role dependencies through the dependencies: parameter.
  • You can pass specific vars to dependency roles to customize their behavior without affecting the playbook’s global scope.
  • Ansible resolves dependencies recursively using a depth-first search approach before executing the main role.
  • By default, Ansible applies deduplication so the same dependency role isn’t run twice; use allow_duplicates: true to disable this feature.
  • Use requirements.yml to manage, document, and download external role dependencies from Ansible Galaxy or Git separately from your project repository.
  • Pin the versions of your dependencies in requirements.yml explicitly to guarantee production environment stability from unexpected changes.

← Previous: Structure Next: Reusability →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact