Understanding resource dependencies in Terraform
When you deploy infrastructure as code, the order in which resources get created matters. A database must exist before your application tries to connect to it. A subnet must exist before you launch an EC2 instance into it. Terraform handles this automatically in most cases, but understanding resource dependencies: a practical Terraform & IaC guide is essential for building reliable, maintainable infrastructure. Knowing how Terraform orders operations prevents race conditions, failed deployments, and hours spent debugging "why did this resource fail to create?"
References create implicit dependencies
The simplest and most powerful dependency mechanism in Terraform is the reference. When you use an attribute from one resource inside another, Terraform automatically detects that dependency and waits for the first resource to finish before creating the second.
Consider a typical AWS setup where you need a security group and an EC2 instance:
resource "aws_security_group" "app" {
name = "app-sg"
description = "Security group for app server"
vpc_id = aws_vpc.main.id
}
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.app.id]
subnet_id = aws_subnet.private.id
tags = {
Name = "app-server"
}
}When Terraform parses this, it sees aws_security_group.app.id referenced in the instance definition. It builds a dependency graph: the security group must be created first, then the instance. This happens without you writing a single line of explicit dependency configuration.
The same applies to data sources. If you query a VPC's ID and use it downstream, Terraform knows to resolve that data source before using it. References work across modules too—if module A outputs a subnet ID that module B uses, the dependency flows automatically.
This is why reference-based dependencies are so elegant: they're self-documenting and virtually impossible to forget. The code itself declares what depends on what.
Using depends_on for hidden dependencies
Not every dependency can be expressed as a reference. Some resources depend on others without any direct attribute linkage. This is where depends_on comes in.
A real example: you're setting up an RDS database and need to ensure an IAM role is fully created before your database's enhanced monitoring kicks in. Or you're creating an S3 bucket and need to ensure a CloudWatch log group exists first, even though the bucket doesn't reference the log group's ID.
resource "aws_cloudwatch_log_group" "rds_logs" {
name = "/aws/rds/mysql"
retention_in_days = 7
}
resource "aws_db_instance" "mysql" {
allocated_storage = 20
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
username = "admin"
password = random_password.db_password.result
skip_final_snapshot = true
enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
depends_on = [aws_cloudwatch_log_group.rds_logs]
}
resource "random_password" "db_password" {
length = 16
special = true
}Here, the RDS instance doesn't directly reference the log group, but we want to ensure it exists before the database attempts to export logs to CloudWatch. The depends_on meta-argument tells Terraform: "don't create this resource until that resource is ready."
Another scenario: you create an application load balancer and want to ensure a VPC endpoint service is available before the load balancer tries to route traffic through it. A wall switch or configuration file might need to be updated before an application starts. These are your depends_on situations.
One important note: use depends_on sparingly. Overusing it can lead to unnecessarily sequential operations when parallelization would be faster. Use it only when a true hidden dependency exists and a reference won't work.
Parallel creation where possible
Terraform's dependency graph enables massive parallelization. If resource A doesn't depend on resource B, and resource B doesn't depend on resource A, Terraform will create them simultaneously. This is why cloud deployments with dozens of independent resources feel fast.
Consider this structure:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}Terraform sees that both subnets depend only on the VPC, so it creates them in parallel. The route table depends on the internet gateway and VPC. Within one apply, you'll see multiple resources being created concurrently.
This parallelization is automatic and requires no configuration from you. The dependency graph algorithm finds the optimal ordering. If you have 50 independent resources, Terraform's parallelization (typically 10 workers by default) will chunk them efficiently.
If your apply feels slow, profile the dependencies. Sometimes a hidden depends_on or an unnecessary reference serializes operations that could run in parallel. Use the graph command (discussed next) to visualize bottlenecks.
Using terraform graph to visualize the order
When dependencies get complex, the terraform graph command generates a dependency graph in DOT format. This is invaluable for understanding how Terraform will order your operations.
Run it like this:
terraform graph | dot -Tpng > graph.pngOr for a simpler text view:
terraform graphThe output shows nodes (resources, modules, providers) and edges (dependencies). Each edge points from a dependency to the dependent. If you see long chains with many sequential edges, you've found a potential bottleneck.
For example, a graph might show:
aws_vpc.main
↓
aws_subnet.public
↓
aws_instance.app
↓
aws_route53_record.appThis linear chain means all four operations happen sequentially. If the subnet creation were independent of the instance (which it shouldn't be, in this case), you could parallelize. The graph makes these patterns obvious.
In larger deployments with modules, the graph becomes a directed acyclic graph (DAG) with many parallel branches. The visualization shows exactly where Terraform can parallelize and where it must wait.
Pro tip: pipe the graph output to GraphViz for a visual representation. Keep the DOT file in your repo or CI logs for post-mortem analysis if a deployment takes longer than expected.
Common mistakes
Over-relying on depends_on: New teams sometimes use depends_on for every cross-resource relationship. This serializes your infrastructure creation and defeats parallelization. Use references whenever possible. Reserve depends_on for truly hidden dependencies that can't be expressed through attributes.
Circular dependencies: If resource A depends on B, and B depends on A (directly or through a chain), Terraform will error. This usually happens with cyclic module dependencies or when you accidentally reference a resource from inside a local-exec provisioner that itself depends on that resource. The error message is clear, but prevention is better. Review your dependency graph before committing.
Forgetting implicit dependencies in data sources: A data source that queries resources by name might not explicitly reference those resources. If you create and then query them in the same apply, the data source might fail because the resource hasn't appeared in the cloud provider's API yet. Use references to make the dependency explicit, or add depends_on.
Assuming resource creation is instant: Some resources are created by the cloud provider asynchronously. You might get an "ID" back immediately, but the resource isn't fully available for 10 seconds. If a downstream resource tries to configure something on the parent resource too quickly, it can fail. Building in explicit depends or using provisioners with retry logic helps here.
Not testing dependency order in staging: A configuration that works in small deployments might expose dependency issues when scaled to production size. Test with realistic resource counts and module structures before going live.
Key takeaways
- References are your primary dependency mechanism—they're automatic, self-documenting, and prevent errors.
depends_onhandles hidden dependencies where attributes can't express the relationship; use it sparingly to preserve parallelization.- Terraform parallelizes resource creation where the dependency graph allows, making deployments faster and more efficient.
- Use
terraform graphto visualize and audit your dependency ordering, especially when deployments feel slow. - Circular dependencies are errors—design your modules and resource relationships to flow in one direction.
- Test dependency order with realistic resource counts before production deployments to catch serialization bottlenecks.






Comments
Loading comments…