Home
Locals
Terragrunt Dependencies: How to Manage Multi-Module Dependencies Correctly

Terragrunt Dependencies: How to Manage Multi-Module Dependencies Correctly

with special guest
Mitchell
Hashimoto
Mitchell Hashimoto headshot

Managing infrastructure across multiple Terraform modules quickly becomes complex. Terragrunt's dependency system solves this by letting modules share outputs, enforcing apply order, and keeping your stack DRY. This guide covers everything teams need to know — from basic wiring to advanced mocking strategies.

What Is a Terragrunt Dependency?

A dependency in Terragrunt is a declared relationship between two terragrunt.hcl files that tells Terragrunt:

  1. Which module to run first — the dependency must be applied before the dependent module.
  2. Which outputs to expose — the dependent module can read output values from the dependency.

This mirrors how Terraform modules pass values to one another, but works across separate state files and directories — a common pattern in large-scale infrastructure where VPCs, databases, and applications live in separate modules.

A Minimal Example

This Terragrunt configuration demonstrates how to use module dependencies to share outputs between infrastructure components. In this case, the App module consumes outputs from the VPC module.

Terragrunt Dependency Configuration
# modules/app/terragrunt.hcl

dependency "vpc" {
config_path = "../vpc"
}

inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}

The dependency block allows Terragrunt to automatically read outputs from another module. This enables clean separation of infrastructure layers while still allowing secure and dynamic data sharing between them.

When you run terragrunt apply inside modules/app/, Terragrunt will:

  • Detect the vpc dependency
  • Read the Terraform state of ../vpc to fetch its outputs
  • Inject those outputs as inputs into the current module

dependency vs dependencies: What's the Difference?

These two blocks are often confused but serve distinct purposes.

Feature dependency dependencies
Fetches Outputs ✅ Yes ❌ No
Enforces Apply Order ✅ Yes ✅ Yes
Use Case Share data and outputs between modules. Declare execution ordering without requiring outputs.

dependency block

Use dependency when you need to pass outputs from one module to another:

This Terragrunt configuration shows how to use a dependency block to consume outputs from an RDS module. It enables clean separation between database provisioning and application configuration while still sharing required values.

Terragrunt RDS Dependency
dependency "rds" {
config_path = "../rds"
}

inputs = {
db_endpoint = dependency.rds.outputs.endpoint
}

The dependency.rds.outputs.endpoint value is dynamically pulled from the RDS module after it is applied. This approach avoids hardcoding infrastructure values and ensures modules remain loosely coupled and reusable.

dependencies block

Use dependencies when you need to enforce ordering but don't need any output values:

This Terragrunt configuration uses the dependencies block to ensure multiple infrastructure components are applied in the correct order. It enforces orchestration across IAM roles and security groups before dependent modules are executed.

Terragrunt Multi-Module Dependency Ordering
dependencies {
paths = ["../iam-roles", "../security-groups"]
}

The dependencies.paths directive ensures Terragrunt processes and applies the referenced modules first. This is useful when infrastructure components must exist (like IAM roles or security groups) before other resources can be safely deployed.

This is useful for modules that must exist before others (e.g., IAM roles, KMS keys) but whose outputs you don't directly consume.

Rule of thumb: Use dependency when you need values. Use dependencies when you only need sequencing.

How run_all Determines Apply Order

When you run terragrunt run-all apply from a root directory, Terragrunt builds a directed acyclic graph (DAG) of all modules based on their dependency and dependencies declarations.

How it works

  1. Terragrunt scans all subdirectories for terragrunt.hcl files.
  2. It resolves each module's declared dependencies into a graph.
  3. Modules with no dependencies (leaf nodes) are applied first, in parallel where possible.
  4. Dependent modules are applied only after all their dependencies complete successfully.

Example stack

This infrastructure layout demonstrates a dependency-driven Terragrunt architecture. Each module builds on previously provisioned components, ensuring correct provisioning order and clean separation of concerns.

Infrastructure Dependency Graph
infra/
├── vpc/
├── rds/ # depends on vpc
├── ecs-cluster/ # depends on vpc
└── app/ # depends on rds and ecs-cluster

In this design, the VPC layer is foundational, providing networking for all other modules. The RDS and ECS cluster are built on top of it, while the application layer depends on both compute and database resources. This layered approach improves scalability, reuse, and safe deployment ordering in infrastructure-as-code systems.

With this structure, run-all apply will:

  1. Apply vpc first
  2. Apply rds and ecs-cluster in parallel (both depend only on vpc)
  3. Apply app last (depends on both)

Running in the right order manually

If you apply modules one by one, you must follow dependency order yourself. Terragrunt will error if a dependency hasn't been applied yet and its outputs are unavailable — unless you configure mocking (see below).

Accessing Dependency Outputs

Once a dependency is declared, its outputs are available via dependency.<name>.outputs.<output_name>.

This Terragrunt configuration demonstrates how an application module consumes outputs from a shared network module. It centralizes VPC and subnet management while allowing downstream modules to remain clean and reusable.

Terragrunt Network Dependency Injection
dependency "network" {
config_path = "../../shared/network"
}

inputs = {
vpc_id = dependency.network.outputs.vpc_id
private_subnets = dependency.network.outputs.private_subnet_ids
public_subnets = dependency.network.outputs.public_subnet_ids
}

The dependency.network.outputs values are dynamically fetched after the network stack is applied. This pattern ensures consistent networking across environments while avoiding duplication of VPC and subnet definitions.

The output names must match exactly what is declared in the dependency module's outputs.tf. Terragrunt fetches these from the remote state of the dependency, not by running Terraform again.

Outputs from nested attributes

If an output is a map or object, you can access nested values using standard HCL expressions:

This Terragrunt configuration shows how to extract structured database connection details from an RDS module using dependency outputs. It allows downstream modules to dynamically consume host and port information.

RDS Connection Data Consumption
inputs = {
db_host = dependency.rds.outputs.connection_info["host"]
db_port = dependency.rds.outputs.connection_info["port"]
}

The connection_info output is assumed to be a map containing database endpoint metadata. Using indexed access like ["host"] and ["port"] enables flexible, structured output handling between modules.

Mocking Dependencies in Tests

When running terragrunt plan in CI, validating new modules, or working offline, you often don't want Terragrunt to actually fetch remote state. Mock outputs solve this.

Basic mock configuration

This Terragrunt configuration demonstrates the use of mock outputs for a VPC dependency. It is commonly used in testing, validation, and planning when the actual dependency has not yet been applied.

VPC Dependency with Mock Outputs
dependency "vpc" {
config_path = "../vpc"

mock_outputs = {
vpc_id = "vpc-mock-12345"
private_subnet_ids = ["subnet-mock-a", "subnet-mock-b"]
}
}

The mock_outputs block allows Terragrunt to simulate dependency outputs during plan-time. This enables independent development and CI validation of downstream modules without requiring the VPC stack to be deployed first.

With mock_outputs defined, if Terragrunt cannot read the dependency's state (e.g., it hasn't been applied yet), it falls back to the mock values.

Controlling when mocks are used

Use mock_outputs_allowed_terraform_commands to restrict mocks to specific commands:

This Terragrunt configuration demonstrates how to use mock outputs with command restrictions. It allows simulated dependency values only during specific Terraform commands like validate and plan.

Conditional Mock Outputs for VPC Dependency
dependency "vpc" {
config_path = "../vpc"

mock_outputs = {
vpc_id = "vpc-mock-12345"
}

mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

The mock_outputs_allowed_terraform_commands setting ensures mock values are only used during non-destructive operations. This prevents accidental reliance on fake infrastructure data during apply while still enabling fast CI validation workflows.

This ensures mocks are only used during plan and validate, never during apply — preventing accidental deploys with fake values.

mock_outputs_merge_strategy_with_state

When a dependency has been partially applied, you may want to merge real outputs with mocks for any missing keys:

This Terragrunt configuration demonstrates a mock output merge strategy, controlling how mocked values are combined with real state outputs. It is useful when new outputs are introduced in dependencies without breaking downstream modules.

VPC Dependency with Shallow Merge Strategy
dependency "vpc" {
config_path = "../vpc"

mock_outputs = {
new_output_added_recently = "mock-value"
}

mock_outputs_merge_strategy_with_state = "shallow"
}

The shallow merge strategy ensures that only top-level keys from mock_outputs are merged with real state outputs. This helps prevent accidental overwriting of existing values while still supporting gradual schema evolution in shared infrastructure modules.

Valid strategies: no_merge (default), shallow, deep.

Does Terragrunt Support OpenTofu?

Yes. Terragrunt fully supports OpenTofu as a drop-in replacement for Terraform. OpenTofu is the open-source fork of Terraform maintained by the Linux Foundation, and Terragrunt is engine-agnostic.

Configuring Terragrunt to use OpenTofu

In your root terragrunt.hcl, set the terraform_binary option:

This configuration shows how Terragrunt can be adapted to use OpenTofu instead of Terraform, along with shared variable handling across common Terraform commands.

Terragrunt Configuration with OpenTofu
terraform {
extra_arguments "common_vars" {
commands = get_terraform_commands_that_need_vars()
}
}

terraform_binary = "tofu"
Alternative Environment Variable Approach
# Set OpenTofu as the Terraform binary for Terragrunt
export TERRAGRUNT_TFPATH=tofu

Setting terraform_binary = "tofu" or using TERRAGRUNT_TFPATH allows Terragrunt to execute OpenTofu transparently. This enables seamless migration from Terraform while preserving existing infrastructure workflows and CI/CD pipelines.

All dependency, dependencies, and run-all features work identically with OpenTofu. Terragrunt detects the binary in use and adapts accordingly. Teams migrating from Terraform to OpenTofu can switch the binary without changing any terragrunt.hcl dependency configuration.

Common Dependency Errors and How to Fix Them

Error: Could not read outputs

Error: Could not read outputs from module ../vpc:

...the state file does not exist

Cause: The dependency module hasn't been applied yet.

Fix: Apply the dependency first (cd ../vpc && terragrunt apply), or add mock outputs for plan/validate stages.

Error: Cycle detected

Error: Cycle detected in dependency graph

Cause: Module A depends on Module B, and Module B depends on Module A (directly or transitively).

Fix: Restructure your modules to remove the circular dependency. Extract shared outputs into a separate "foundation" module that both A and B depend on.

Error: Output does not exist

Error: Output "subnet_ids" does not exist in module ../vpc

Cause: The output name in your dependency block doesn't match the actual output name in the dependency's outputs.tf.

Fix: Check the exact output names in the dependency's Terraform configuration and update your dependency block accordingly.

Error: dependency config_path not found

Error: Config file not found at path ../shared/vpc/terragrunt.hcl

Cause: The config_path in your dependency block points to a non-existent directory or the terragrunt.hcl file is missing.

Fix: Verify the relative path is correct from the current module's location, and confirm a terragrunt.hcl exists at the target path.

Best Practices for Terragrunt Dependencies

1. Keep dependency graphs shallow

Deep chains (A → B → C → D → E) slow down run-all and make failures hard to trace. Aim for 2–3 levels of depth in most stacks. Flatten where possible by combining tightly coupled modules.

2. Always define mock outputs for CI

Every dependency block used in a module that runs plan in CI should have mock_outputs and mock_outputs_allowed_terraform_commands = ["validate", "plan"]. This prevents CI pipelines from failing when lower-level infrastructure hasn't been applied in the target environment yet.

3. Use dependencies for non-output ordering

Don't create fake outputs just to enforce ordering. If a module like iam-roles doesn't expose outputs you need, use the dependencies block — it's clearer and avoids confusion about what data is being shared.

4. Co-locate related modules in a stack directory

Organize your repository so that modules that depend on each other are close in the directory tree. This makes config_path references short and readable, and makes run-all commands predictable when run from a stack root.

5. Version-pin your dependency modules

When depending on a shared module (e.g., from a central infra-modules repo), pin to a specific version or git ref in your source block. This prevents unexpected breakage when shared modules change.

6. Validate dependency outputs explicitly

If your module requires specific outputs, add validation in the consuming module's variable definitions (with validation blocks in Terraform) rather than relying on silent nulls or type errors at apply time.

7. Document dependency intent

Add a comment above each dependency block explaining why it exists, not just what it points to:

This Terragrunt snippet defines a VPC dependency used to supply networking information such as VPC ID and private subnets to downstream applications. The comment clarifies that the application runs inside a private network.

VPC Dependency Configuration
# Needed for VPC ID and private subnets — app runs in private network only

dependency "vpc" {
config_path = "../vpc"
}

The dependency "vpc" block allows this module to consume outputs from the VPC stack, ensuring all application resources are correctly deployed into a secure, isolated network environment.

Summary

Terragrunt's dependency system is one of its most powerful features for managing real-world infrastructure. By understanding the difference between dependency and dependencies, how run-all builds its execution graph, and how to safely mock outputs in non-production stages, teams can build reliable, maintainable multi-module stacks.

Schedule a technical demo
See env zero in action
Schedule demo

Related Content

All articles