Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terraform depends_on: When and Why

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.

Implicit vs Explicit DependenciesVPCaws_vpc.mainSubnetsubnet_id refEC2implicit chainIAM Roleno attr on LambdaLambdaneeds depends_onReference creates implicit edge automaticallydepends_on adds edge when no reference exists
Terraform depends_on adds graph edges that attribute references cannot express, such as IAM roles attached outside direct resource arguments.

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.

Apply Order With depends_onStep 1IAM roleStep 2Policy attachStep 3Log groupStep 4Lambda fnCommon depends_on scenarios• IAM policy attachment before compute• Log group before Lambda first write• Parameter group before RDS modify• Service-linked role before service• Null resource bootstrap hooks• Module output timing gaps
Terraform depends_on serializes create and destroy steps when provider APIs require prerequisites that HCL references do not capture.

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.

SignalPrefer referencePrefer depends_on
Attribute available on upstream resourceYes — use the attributeNo
IAM attachment not reflected in role ARNNoYes
Ordering for destroy safety onlySometimes lifecycle rulesYes, if lifecycle is insufficient
Cross-module with no shared outputAdd output firstYes, temporary bridge
Flaky apply fixed by re-runningInvestigate root causeMaybe — verify API timing
Data source reads existing infraFix state or importRarely

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.

Implicit vs Explicit ComparedImplicit (preferred)Created from attribute refsSelf-documenting HCLEnables value passingWorks with for_each keysPlan shows data flowExample: subnet_id = aws_subnet.a.idExplicit depends_onOrdering without data tieNeeds comment for whyReduces parallelismHides missing outputsStill valid last resortExample: depends_on = [aws_iam_role_policy.x]
Implicit dependencies carry values and ordering; Terraform depends_on carries ordering alone and should appear sparingly in production modules.

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.

  1. Confirm whether an attribute reference already links the resources.
  2. Search provider issues for propagation delays on the failing resource type.
  3. Add the smallest depends_on set that fixes the race—one attachment, not the whole module.
  4. Re-run apply twice; intermittent success confirms timing, not config drift.
  5. 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.

Debug depends_on FailuresFailed applyRead errorterraform graphFind parallelMinimal fixProduction gotchas I watch forStale remote state locks masking partial appliesDrift from manual console edits — see drift detectionCI runner missing same provider version pinWorkspace env vars changing provider alias
Debug Terraform depends_on issues by tracing the plan graph, confirming races, and applying the smallest explicit dependency that fixes ordering.

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 graph and 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

It is a meta-argument that adds manual edges to Terraform's dependency graph when attribute references cannot express required create or destroy ordering.

Only when one resource must finish before another, but no attribute creates an implicit link—common with IAM propagation, bootstrap scripts, or provider-side ordering.

Yes. A module block accepts depends_on listing whole module addresses or individual resources inside another module's scope.

Implicit dependencies come from expressions—any reference like aws_instance.web.id inside another resource creates an automatic graph edge that carries both ordering and values. Terraform depends_on adds ordering edges alone, without data coupling. Prefer implicit links because they document wiring and preserve parallelism. Explicit depends_on is appropriate when the platform enforces serial creation or propagation delays that HCL references do not capture, such as IAM policy attachments that are not reflected in a role ARN passed to Lambda.

Not by default. depends_on waits for the listed dependency to complete its current Terraform operation—typically when the provider API returns success on create or destroy. It does not pause until a resource passes health checks unless the provider maps that behaviour into create completion. If your apply fails because a database or service is not yet accepting connections, ordering alone may be insufficient. Pair depends_on with correct configuration, and investigate provider documentation for propagation delays rather than assuming health-gated waits.

Terraform creates an implicit dependency on the IAM role ARN, but not necessarily on separate policy attachment resources. AWS IAM propagation is eventually consistent—the attachment API may return success before every edge location honours the permission. First-invoke AccessDenied errors often trace to missing depends_on on aws_iam_role_policy_attachment or related inline policy resources. The same pattern affects ECS task roles, EKS pod identity, and cross-account assume-role chains. Add depends_on on the specific attachment, not the entire IAM module, and document the race in your module README.

Avoid depends_on when an attribute reference already links the resources—a subnet_id or role ARN makes intent self-documenting and keeps parallelism. Overusing explicit dependencies slows every plan and apply by forcing serial steps Terraform could run concurrently. It also hides missing references that future maintainers must reverse-engineer. If you are adding a fifth depends_on in one module, pause and redesign module boundaries or outputs instead. Use depends_on as a last resort after confirming the provider API requires ordering that expressions cannot express, not as a default fix for flaky applies.

When module B needs something module A creates, expose a typed output and pass it as a variable instead of depending on the whole module. For example, if module network creates a private hosted zone and module database needs DNS records there, pass zone_id = module.network.private_zone_id rather than depends_on = [module.network]. Terraform then derives an implicit edge from the expression and future readers see data flow in variables and outputs. Treat temporary module-level depends_on as debt—open a follow-up refactor once outputs are modelled.

Generally no. Data sources refresh during plan, and chaining depends_on = [data.aws_ami.latest] to delay an instance is a common mistake. Data sources should not gate creation of Terraform-managed resources unless you fully understand refresh timing. Prefer passing the AMI ID through a local value or an explicit image filter output. Policy scanners such as Checkov may flag unusual dependency graphs—treat those flags as prompts to review intent, not automatic rewrites. Fix state or import workflows when the issue is discovering existing infrastructure, not ordering.

Start with the plan graph, not guesswork. Run terraform plan -out=tfplan and inspect the saved plan, or enable detailed logging when CI reproduces the race. Confirm whether an attribute reference already links the failing resources. Search provider issues for propagation delays on that resource type. Add the smallest depends_on set that fixes the race—one IAM attachment, not an entire module. Re-run apply twice; intermittent success confirms timing, not config drift. Use terraform graph and terraform graph -type=plan to render DOT files for Graphviz and look for parallel branches that should be serial.

Yes. Creation order reverses on destroy, so 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—create_before_destroy and prevent_destroy address different concerns than ordering meta-arguments. If destroy races appear, trace the graph and add explicit dependencies on the specific prerequisite blocking deletion, rather than blanket module-level lists that slow unrelated resources.

Intermittent success usually confirms an API timing or propagation race, not config drift. IAM attachments, service-linked roles, and bootstrap hooks are frequent culprits—the cloud API returns success before downstream consumers are ready. Re-running apply may succeed because propagation finished between attempts. Before adding depends_on everywhere, inspect the plan graph, identify the smallest missing prerequisite, and add one targeted explicit dependency. Document the fix in the module README and plan a follow-up to replace it with outputs if possible.

No. Terraform provisions infrastructure; Ansible installs packages and tunes services. The hand-off belongs in your CI pipeline as separate stages—not a fragile depends_on chain across tool boundaries. On VPS-style stacks, implicit Terraform dependencies usually suffice for host creation. Complex cloud IAM and bootstrap side effects are where explicit depends_on earns its place inside Terraform modules. Keep application deploys—such as Laravel releases via Deployer—and infrastructure applies in distinct pipeline stages so unrelated ordering bugs do not block releases.

List whole resource or module addresses—not individual attributes. On a resource block, reference complete addresses like aws_iam_role_policy_attachment.lambda_logs and aws_cloudwatch_log_group.lambda. On a module block, use module.other or a resource inside that module's scope. The HashiCorp Terraform depends_on meta-argument guide is the source of truth for syntax across Terraform 1.x releases. Add a one-line comment or README note explaining why each explicit dependency exists, so maintainers are not forced to reverse-engineer ordering from a bare list.

References to indexed instances—such as aws_instance.web[0].id or aws_subnet.private["a"].id—still create implicit edges automatically. If you depend on an entire resource type with multiple instances, list the whole resource or use splat expressions carefully. Refactoring keys in for_each or count breaks dependency addresses silently until the next plan surfaces the error. Read your for_each versus count strategy before mixing either meta-argument with depends_on, and prefer attribute references on specific instances when values must flow downstream.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: