Terraform’s ability to manage complex infrastructure stacks relies fundamentally on its dependency graph. This graph determines the precise order in which resources are created, updated, or destroyed, ensuring that prerequisites exist before dependent components are provisioned. In large-scale infrastructure-as-code (IaC) projects, a misunderstanding of how Terraform resolves these relationships can lead to race conditions, failed apply operations, and unnecessary resource replacements. The core of Terraform’s dependency management lies in the distinction between implicit dependencies, which are automatically inferred from attribute references, and explicit dependencies, which are manually defined using the depends_on meta-argument. While implicit dependencies are the preferred method for almost all use cases, there are specific architectural scenarios where explicit ordering is required. Furthermore, the logic of dependency resolution extends beyond single configurations into multi-module architectures and even across different Terraform configurations, adding layers of complexity that engineers must navigate to maintain reliable provisioning pipelines.
The Foundation of Terraform Resource Dependencies
Resource dependencies describe how different resources and data sources within Terraform configurations relate to one another. These relationships are the building blocks of the dependency graph that Terraform constructs during the plan and apply phases. Proper dependency management is critical to avoid race conditions and ensure reliable provisioning in complex infrastructure setups. When Terraform builds this graph, it analyzes the configuration files to identify which resources must exist before others can be successfully created or modified.
The dependency graph operates on a strict logical sequence. For example, a virtual network must exist before a subnet can be created within it, and a subnet must exist before a compute instance can be assigned to it. If this order is violated, the API calls to the cloud provider will fail because the parent resource does not yet exist. Terraform handles this automatically by mapping these relationships, but the method by which these relationships are identified—whether through direct attribute interpolation or manual declaration—has significant implications for code maintainability and execution performance.
Consider a basic Azure Resource Manager (AzureRM) configuration. The first configuration below demonstrates a healthy dependency structure using implicit references:
```hcl
resource "azurermresourcegroup" "default" {
name = "rg-spacelift-resources"
location = "swedencentral"
}
resource "azurermstorageaccount" "backup" {
name = "stspaceliftbackup"
resourcegroupname = azurermresourcegroup.default.name
location = azurermresourcegroup.default.location
accounttier = "Standard"
accountreplication_type = "LRS"
}
```
In this setup, the storage account references the resource group’s name and location attributes. Terraform immediately recognizes that the storage account cannot be created until the resource group is fully provisioned. In contrast, a configuration that hardcodes the resource group name, as shown below, creates no dependency:
```hcl
resource "azurermresourcegroup" "default" {
name = "rg-spacelift-resources"
location = "swedencentral"
}
resource "azurermstorageaccount" "backup" {
name = "stspaceliftbackup"
resourcegroupname = "rg-spacelift-resources"
location = "swedencentral"
accounttier = "Standard"
accountreplication_type = "LRS"
}
```
Although both configurations look similar at first glance, the second one is fragile. If the resource group name changes, the storage account will still attempt to use the hardcoded string, potentially leading to errors or state drift. More critically, Terraform does not see a dependency in the second example, meaning it might attempt to create the storage account in parallel with or before the resource group, leading to immediate failure. This distinction highlights why implicit dependencies are the standard practice in Terraform engineering.
Implicit Dependencies: Automatic Discovery and Preferred Practice
Implicit dependencies are the primary mechanism by which Terraform determines execution order. These dependencies are automatically discovered by Terraform by analyzing resource attributes. When one resource refers to another using interpolation syntax, Terraform recognizes this as a dependency. In other words, implicit dependencies in Terraform are created when one resource property references another resource's property or output.
This automatic inference is considered the preferred way of handling dependencies because it is self-documenting and tightly coupled with the data flow of the infrastructure. When you reference a resource attribute, you are explicitly telling Terraform that the data value for the current resource depends on the existence of the referenced resource.
The Mechanics of Implicit References
The mechanics of implicit dependencies rely on expression references. If Resource A references Resource B, Terraform adds an edge to the dependency graph from B to A, indicating that B must be created or updated before A. This is true for any attribute reference, whether it is a simple string interpolation or a complex nested block reference.
Consider the following AWS example, which illustrates a chain of implicit dependencies:
```hcl
VPC must exist before subnets
resource "awsvpc" "main" {
cidrblock = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
Implicit dependency on awsvpc.main through vpcid reference
resource "awssubnet" "public" {
vpcid = awsvpc.main.id # This creates the dependency
cidrblock = "10.0.1.0/24"
tags = {
Name = "public-subnet"
}
}
Implicit dependency chain: VPC -> Subnet -> Instance
resource "awsinstance" "web" {
ami = var.amiid
instancetype = "t3.micro"
subnetid = aws_subnet.public.id # Depends on subnet
tags = {
Name = "web-server"
}
}
```
In this scenario, the aws_instance depends on the aws_subnet because it references aws_subnet.public.id. The aws_subnet depends on the aws_vpc because it references aws_vpc.main.id. Terraform’s planner traces these references to build a directed acyclic graph (DAG). The resulting execution order is strictly: VPC creation, followed by Subnet creation, followed by Instance launch. This chain is resolved automatically without any explicit declaration, demonstrating the efficiency of implicit references.
The preference for implicit dependencies stems from their reliability. Because the dependency is tied to the actual data flow, Terraform understands exactly why the ordering is necessary. If the reference is removed, the dependency is removed, and the graph is updated accordingly. This dynamic nature reduces the risk of stale dependencies that could block necessary parallelism or cause unnecessary serial execution.
Explicit Dependencies and the depends_on Meta-Argument
While implicit dependencies handle the vast majority of ordering requirements, there are scenarios where a resource or module must wait for another to complete, even if no attribute is referenced. In these cases, Terraform’s automatic inference fails because there is no data dependency to track. This is where explicit dependencies, defined via the depends_on meta-argument, become necessary.
The Role of depends_on
The depends_on meta-argument creates an explicit dependency between resources or modules when Terraform cannot infer the relationship automatically. It is a manual declaration that forces a specific execution order. However, the use of depends_on is often discouraged by experienced Terraform engineers because it can mask design flaws and create hidden ordering constraints.
Terraform automatically infers dependencies from resource references, so explicit depends_on is rarely needed. When overused, it creates hidden ordering constraints that make configurations harder to maintain and can force unnecessary resource replacements during apply. For instance, if you add a depends_on to a resource that does not actually use the referenced resource’s data, you are introducing a serial bottleneck that has no logical basis in the infrastructure’s data flow.
When to Use Explicit Dependencies
There are valid use cases for depends_on. The primary scenario is when you need to enforce execution order without direct references. A common example involves waiting for a background process or a manual action, though Terraform does not natively support "wait for manual action" in standard resources. More commonly, it is used when a resource relies on a side effect of another resource that is not captured in the state or attributes.
Another valid use case is when dealing with modules. If Module A needs to be fully applied before Module B starts, but Module B does not consume any outputs from Module A, you can use depends_on in the module block. However, this should be a last resort. The preferred approach is to design the modules so that they share data through outputs and inputs, thereby creating an implicit dependency.
The following table summarizes the differences between implicit and explicit dependencies, highlighting when each should be used:
| Feature | Implicit Dependencies | Explicit Dependencies (depends_on) |
|---|---|---|
| Definition Method | Automatic via attribute references | Manual via depends_on meta-argument |
| Discovery | Inferred by Terraform parser | Explicitly declared by the engineer |
| Preferred Usage | Yes, for all data-dependent resources | No, only for non-data-dependent ordering |
| Maintainability | High; changes with data flow | Lower; static ordering constraints |
| Risk Profile | Low; reflects logical infrastructure needs | Higher; can hide design issues |
| Example | vpc_id = aws_vpc.main.id |
depends_on = [aws_db_instance.main] |
Dependencies Across Modules and Configurations
Terraform’s dependency logic extends beyond single resource blocks into the realm of modules and even multiple Terraform configurations. Understanding how dependencies are resolved in these contexts is essential for organizing large-scale infrastructure code.
Module Dependencies
Terraform module dependencies refer to the order in which Terraform provisions resources across modules, based on how outputs and inputs are linked. A module depends on another when it consumes its outputs as inputs, establishing an implicit dependency.
When Module B uses an output from Module A as an input, Terraform automatically knows that Module A must be applied before Module B. This is the recommended pattern for inter-module communication. It ensures that the data flow is explicit and that the dependency is tied to the actual values being passed.
For example, if a network module outputs a VPC ID and a compute module uses that VPC ID to create instances, the compute module implicitly depends on the network module. Terraform will resolve this by ensuring the network module’s resources are fully applied before the compute module’s planning and application phases begin.
Should you use depends_on on modules? In most cases, you should not use depends_on with modules. It is primarily intended for resources, not modules, and its behavior with modules is limited and potentially unreliable. If you need to enforce a dependency between resources in separate modules, it’s preferable to pass outputs from one module as inputs to the other. Forcing a depends_on between modules without data flow can lead to unexpected state management issues and makes the infrastructure harder to reason about.
If no such reference exists but execution order is important, you can use the depends_on argument in the module block to enforce that dependency explicitly. However, this should be documented clearly with comments explaining why the data-independent ordering is required.
Cross-Configuration Dependencies
Some dependencies will appear in a single Terraform configuration and across multiple Terraform configurations. This is a critical concept for teams that manage infrastructure using multiple state files or separate Terraform runs (e.g., one for networking, one for compute).
Terraform itself does not natively manage dependencies across separate Terraform configurations or state files during a standard apply. If Configuration A creates a VPC and Configuration B creates instances in that VPC, Terraform does not automatically know that Configuration A must run before Configuration B. This is because they are independent units of work.
Managing dependencies across configurations requires external orchestration. Tools like CI/CD pipelines, Spacelift, or other Infrastructure as Code platforms must be configured to run the configurations in the correct order. For example, a pipeline job that applies the network configuration must complete successfully before the job that applies the compute configuration begins.
Alternatively, engineers can use remote state referencing to pull outputs from one configuration into another. If Configuration B reads the VPC ID from Configuration A’s state using the data "terraform_remote_state" data source, an implicit dependency is established within Configuration B’s logic, but the cross-configuration ordering still relies on the pipeline ensuring Configuration A is applied first.
OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6. OpenTofu maintains the same core dependency logic, so the principles of implicit and explicit dependencies apply identically. Teams using OpenTofu benefit from the same automated graph resolution capabilities, ensuring that resource ordering is handled consistently regardless of the underlying engine.
Best Practices for Dependency Management
To keep your configuration clean and performant, engineers should adhere to specific best practices regarding dependency management.
- Prefer Implicit References: Always use attribute references to establish dependencies. This ensures that the dependency is tied to the data flow and is automatically managed by Terraform.
- Avoid
depends_onfor Resources: Only usedepends_onfor resources when there is no possible attribute reference. Document the reason for its use. - Design Modules for Data Sharing: When connecting modules, pass outputs as inputs. Avoid
depends_onbetween modules unless absolutely necessary for non-data-dependent ordering. - Orchestrate Cross-Configuration Ordering: Use CI/CD pipelines or IaC platforms to manage the order of separate Terraform configurations. Do not rely on Terraform to automatically sequence independent state files.
- Visualize the Graph: Use tools to visualize the dependency graph (e.g.,
terraform graph -type=plan) to debug ordering issues and identify unexpected serializations.
Understanding how dependencies work helps you debug ordering issues and design better infrastructure code. When a terraform apply fails due to a resource not being found, checking the dependency graph is the first step. Often, the issue is a missing implicit reference or a hardcoded value that breaks the automatic inference.
Conclusion
Terraform’s dependency graph is the backbone of its state management and execution engine. Implicit dependencies, created automatically through attribute references, are the gold standard for managing resource ordering. They provide a self-documenting, maintainable, and logically sound way to ensure infrastructure is provisioned in the correct sequence. Explicit dependencies, utilizing the depends_on meta-argument, serve a narrow but necessary purpose for enforcing execution order when data references are not possible. However, their misuse can lead to brittle configurations and performance bottlenecks.
As infrastructure scales, the complexity of dependencies grows, extending into multi-module architectures and cross-configuration workflows. Engineers must master not only the internal mechanics of Terraform’s graph but also the external orchestration required to manage dependencies across separate state files. By adhering to best practices—prioritizing implicit references, designing modules for data sharing, and leveraging CI/CD for cross-configuration ordering—teams can build resilient, efficient, and predictable infrastructure pipelines. The ultimate goal is to let Terraform do the heavy lifting of order resolution, reserving manual intervention for the few edge cases where automatic inference is insufficient.