
September 10, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Terraform depends_on: When and Why matters the moment an apply succeeds on paper but breaks in production. Terraform builds a dependency graph from resource references, yet some side effects never appear in attribute wiring. A database parameter group may need to exist before an instance attaches it. A Lambda may need IAM propagation before the first invoke. On real deployments—whether you provision VPS hosts for Laravel or wire CI runners—I have seen teams reach for depends_on too late, after a flaky apply already burned a release window. This guide explains what the meta-argument actually does, when it earns a place in your modules, and when a cleaner reference is the right fix.
The topic sits inside broader infrastructure as code with Terraform practice. If you maintain application stacks alongside Linux system administration work, dependency ordering is not academic—it decides whether your database, cache layer, and app servers come up in a safe sequence. Read this alongside Terraform lifecycle meta-arguments and safe state management for a complete picture.
What does Terraform depends_on actually do?
depends_on is a meta-argument you attach to a resource, module, or data source. It tells Terraform: do not create or destroy this object until the listed dependencies complete their current operation. Terraform still computes an implicit graph from expressions like aws_instance.web.id. The meta-argument adds edges that expressions alone cannot express.
Think of it as a manual arrow on the dependency graph. Without that arrow, Terraform may schedule two resources in parallel even when the cloud API expects serial creation. The provider documentation often hints at these cases, but the HCL does not always make the requirement obvious.
Syntax on resources and modules
On a resource block, list whole resource addresses—not individual attributes:
resource "aws_lambda_function" "app" {
function_name = "api-handler"
role = aws_iam_role.lambda_exec.arn
runtime = "provided.al2023"
handler = "bootstrap"
depends_on = [
aws_iam_role_policy_attachment.lambda_logs,
aws_cloudwatch_log_group.lambda,
]
} On a module block, the same rule applies. Every entry must be a full resource or module address inside that module's scope, or a module you call from the parent:
module "network" {
source = "./modules/vpc"
depends_on = [
aws_iam_service_linked_role.rds,
]
} Official reference: HashiCorp documents meta-arguments in the Terraform depends_on meta-argument guide. Treat that page as the source of truth for syntax changes across Terraform 1.x releases.
What depends_on does not do
It does not wait for a resource to reach a particular health state unless the provider maps that into create completion. It does not replace create_before_destroy from the lifecycle block. It does not fix incorrect security group rules or missing subnet routes. If the underlying API call is wrong, ordering alone will not save the apply.
It also does not help data sources discover resources Terraform never created. Use terraform import workflows—see bringing existing resources under Terraform control—when state and reality diverge.
When should you use Terraform depends_on?
Reach for explicit dependencies when Terraform cannot infer ordering from expressions, but the platform still enforces it. The pattern shows up often in multi-tier stacks where I deploy application servers, managed databases, and background workers in one root module.
IAM and permission propagation
AWS IAM is the classic case. A Lambda references a role ARN, so Terraform creates an implicit dependency on the role itself. It may not wait for an attached policy or an inline policy document on a separate resource. First-invoke failures often trace back to missing depends_on on the attachment resource.
The same pattern appears with ECS task roles, EKS pod identity, and cross-account assume-role chains. Propagation is eventually consistent. Terraform considers the attachment "created" when the API returns success, not when every edge location accepts it.
Side-effect resources without return values
Some resources exist only to trigger platform behaviour. A null_resource with a local-exec provisioner might seed an S3 bucket policy template. Downstream resources do not read an attribute from it. Without depends_on, Terraform may run the consumer first and fail.
I prefer keeping bootstrap logic inside modules with clear outputs when possible. When that is impossible, explicit ordering is acceptable. Document why in a one-line comment in the module README—not only in chat history.
Destroy ordering for tightly coupled stacks
Creation order reverses on destroy. Explicit dependencies also serialize teardown. That matters when a security group cannot delete while ENIs remain, or when a subnet still holds a NAT gateway.
Pair depends_on with lifecycle rules where appropriate. The lifecycle meta-argument guide covers create_before_destroy and prevent_destroy in depth.
Module-level ordering across teams
When a root module calls child modules that do not reference each other, Terraform schedules them concurrently. If module B assumes module A already registered a DNS zone or created a service-linked role, add depends_on = [module.a] on module B.
This shows up in reusable Terraform modules published for multiple products. Without the meta-argument, CI plans look fine until production traffic hits a race on the first apply after a cold start.
When should you avoid Terraform depends_on?
Overusing depends_on slows every plan and apply. It removes parallelism Terraform would otherwise exploit. Worse, it hides missing references that would make the graph self-documenting. A maintainer reading subnet_id = aws_subnet.app.id understands wiring instantly. A bare depends_on list without comment forces archaeology.
| Signal | Prefer reference | Prefer depends_on |
|---|---|---|
| Attribute available on upstream resource | Yes — use the attribute | No |
| IAM attachment not reflected in role ARN | No | Yes |
| Ordering for destroy safety only | Sometimes lifecycle rules | Yes, if lifecycle is insufficient |
| Cross-module with no shared output | Add output first | Yes, temporary bridge |
| Flaky apply fixed by re-running | Investigate root cause | Maybe — verify API timing |
| Data source reads existing infra | Fix state or import | Rarely |
Use the table as a decision gate during code review. If you are about to add a fifth depends_on in one module, pause. The module boundary or data flow probably needs redesign.
Replacing depends_on with outputs
Suppose module network creates a private hosted zone and module database needs records there. Instead of:
module "database" {
source = "./modules/rds"
depends_on = [module.network]
} Expose a zone ID output and pass it:
module "database" {
source = "./modules/rds"
zone_id = module.network.private_zone_id
} Terraform now has an implicit edge and a typed contract between modules. Future readers see the data flow in variables, locals, and outputs rather than a side channel.
Anti-pattern: depending on data sources for creation order
Data sources refresh during plan. They should not gate creation of resources Terraform manages unless you fully understand refresh timing. A common mistake is chaining depends_on = [data.aws_ami.latest] to delay an instance. Pass the AMI ID through a local value instead, or use an explicit image filter output.
Policy scanners such as those described in Checkov scans for Terraform misconfigurations may flag unusual dependency graphs. Treat flags as prompts, not automatic rewrites.
How does depends_on differ from implicit dependencies?
Implicit dependencies come from expressions. Any reference to resource_type.name.attribute inside another resource creates an edge. Terraform's graph builder walks those references automatically. Explicit dependencies add edges without data coupling.
Interaction with count and for_each
References to indexed instances—aws_instance.web[0].id or aws_subnet.private["a"].id—still create implicit edges. If you depend on an entire resource type with multiple instances, list the whole resource or use splat expressions carefully.
Read for_each versus count before mixing either with depends_on. Refactoring keys breaks dependency addresses silently until the next plan.
Provider configuration dependencies
depends_on cannot reference provider blocks directly. If two resources use different provider aliases—common in multi-region or multi-account setups—ordering still flows through resources, not provider meta-settings. See provider aliases for multi-cloud for alias wiring patterns.
How do you debug Terraform dependency ordering problems?
Start with the plan graph, not guesswork. Run terraform plan -out=tfplan and inspect the saved plan, or enable detailed logging when CI reproduces a race.
- Confirm whether an attribute reference already links the resources.
- Search provider issues for propagation delays on the failing resource type.
- Add the smallest
depends_onset that fixes the race—one attachment, not the whole module. - Re-run apply twice; intermittent success confirms timing, not config drift.
- Document the dependency in the module README and open a follow-up to replace it with an output if possible.
Reading the dependency graph
Terraform 1.x can render a graph file for visualization tools:
terraform graph > graph.dot
terraform graph -type=plan > plan.dot Feed the DOT file into Graphviz locally, or paste nodes into an internal viewer. Look for parallel branches that should be serial. Compare against the Terraform internal dependency graph documentation to understand how walk and destroy phases differ.
CI pipelines and remote state
Pipelines that run terraform apply -auto-approve amplify ordering bugs because nobody watches the middle steps. Wire plans through Terraform CI/CD with GitHub Actions or GitLab equivalents, store state in a remote backend—S3 with locking is a common pattern—and pin provider versions per provider version pinning guidance.
On projects where I ship Laravel apps with Deployer and separately manage cloud primitives with Terraform, I keep application deploys and infra applies in distinct pipeline stages. Application config should not rerun because a unrelated IAM attachment raced ahead of a database parameter group.
Relating depends_on to Ansible and post-provision config
Terraform provisions; configuration management tools install packages and tune services. If you use Ansible after Terraform creates hosts, the hand-off belongs in the pipeline—not a fragile depends_on chain across tool boundaries. Compare responsibilities in Terraform versus Ansible.
For VPS-style stacks, Terraform for VPS provisioning shows simpler graphs where implicit dependencies usually suffice. Complex cloud IAM is where explicit ordering earns its keep.
State, workspaces, and environment isolation
Wrong workspace selection does not look like a dependency bug, but the symptoms match: resources appear missing, plans want to recreate everything. Validate workspace context before blaming depends_on. Multi-environment patterns live in Terraform workspaces and environments and Terragrunt for DRY configs.
When state grows large, dependency analysis slows. Split stacks by blast radius—network, data plane, app plane—and use remote state data sources instead of one monolithic root module. Multi-cloud state management covers federation patterns without forcing every resource into a single graph.
Relating back to application delivery
On a production Laravel stack, Terraform might provision RDS, ElastiCache, and an autoscaling group while GitLab CI deploys PHP code. The database parameter group must exist before the instance accepts connections. Queue workers depend on Redis endpoints. Those are legitimate depends_on candidates when module outputs are not yet modeled.
Projects like Adventure Third Pole Trek run booking workloads that cannot afford a half-provisioned database on peak season traffic. Ordering is operational safety, not HCL pedantry. Pair infra work with enterprise application development when the application and cloud stack evolve together.
Validate JSON IAM policy documents with a JSON formatter before they enter Terraform variables. Syntax errors there surface late and mimic dependency failures.
Key Takeaways
- Prefer attribute references for dependencies; they document data flow and preserve parallelism.
- Use Terraform depends_on when APIs require ordering that HCL references cannot express—IAM attachments, bootstrap hooks, service-linked roles.
- Keep each depends_on list minimal; depend on the specific prerequisite resource, not entire modules, unless module outputs are impossible short term.
- Debug with
terraform graphand targeted re-applies before blanket depends_on sprawl. - Pair explicit dependencies with remote state locking, provider pins, and CI plan review to catch races early.
- Track follow-up refactors to replace depends_on with module outputs so the next maintainer reads intent in values, not hidden edges.
People Also Ask
Can depends_on reference a module?
Yes. A module block accepts depends_on = [module.other] or individual resources inside another module when addresses are visible in scope. The dependent module waits until listed modules or resources complete their current operation. Prefer passing outputs into arguments instead when values are available.
Does depends_on affect destroy order?
Yes. Terraform reverses creation order on destroy. Explicit dependencies serialize teardown the same way. That prevents delete failures when a parent resource still holds children, such as subnets with active ENIs or roles still attached to running functions.
Is depends_on the same as a provisioner?
No. Provisioners run scripts on individual resources during create or destroy. depends_on only adjusts graph scheduling; it does not execute commands. Use provisioners sparingly; external data sources or pipeline steps often age better in production.
Why does my apply succeed locally but fail in CI?
CI often runs colder accounts, parallel jobs, or different provider versions. Timing races surface under load. Compare provider lock files, enable plan artifacts, and inspect whether CI omits a dependency edge that local state already satisfied from a previous partial apply.
Apply dependency discipline on your next Terraform change
Terraform depends_on: When and Why boils down to a simple rule: express relationships with references first, then add explicit ordering for the gaps providers leave open. That discipline keeps plans fast, modules readable, and production applies boring—which is exactly what you want when infra sits under a live product.
Audit your root modules this week. Search for depends_on, confirm each entry still lacks a reference alternative, and trim anything redundant. Wire plans through locked remote state, document the remaining edges, and schedule refactors where outputs can replace hidden ordering.
Need help untangling IaC alongside application delivery, VPS hardening, or CI pipelines? Contact us to review your Terraform layout, or explore drift detection strategies and OpenTofu if you are evaluating toolchain options for 2026.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

