Skip to content
Infrastructure as Code iac concepts 4 min read

Resource Ordering

Terraform does not create, update, or destroy resources in the order you happen to write them in your configuration. Instead it derives a correct order automatically from the relationships between resources. Most of that ordering is inferred for you simply by referencing one resource’s attributes inside another, and you only reach for an explicit depends_on when a real dependency exists that Terraform cannot see. Understanding how this works is the difference between configurations that “just apply” and ones that fail with race conditions or unresolvable cycles.

How Terraform decides what to do first

Before doing anything, Terraform builds a dependency graph of every resource, data source, provider, and output. It then walks that graph in topological order: a node is only acted on once everything it depends on has finished. Independent branches of the graph are processed in parallel (ten concurrent operations by default, tunable with -parallelism=N).

The key insight is that ordering is a property of your references, not of file layout or block position. You can define a security group below the instance that uses it, in a different file entirely, and Terraform will still create the security group first.

Implicit dependencies via references

The primary way to express order is the most natural one: when resource B reads an attribute of resource A, Terraform records an edge “B depends on A.” This is called an implicit dependency, and it is by far the most common form.

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "app" {
  # Referencing aws_vpc.main.id creates the dependency edge.
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.app.id
}

Here Terraform learns the order aws_vpc.main → aws_subnet.app → aws_instance.web purely from the .id references. Note that aws_vpc.main.id is not even known until the VPC is created, so Terraform must wait — the value is a placeholder during planning and gets resolved during apply. On destroy, the same graph is walked in reverse: the instance is torn down first, then the subnet, then the VPC.

Prefer implicit dependencies wherever possible. They are self-documenting and stay correct automatically as your configuration evolves. Reach for depends_on only when there is genuinely no attribute to reference.

Explicit dependencies with depends_on

Sometimes resource B depends on resource A but never references any of its attributes. The dependency is real at the cloud-provider level but invisible in HCL — for example, an application needs an IAM policy to be attached before it can use a role, even though the application config doesn’t read the attachment’s outputs. For these hidden dependencies, declare the edge explicitly.

resource "aws_iam_role" "app" {
  name               = "app-role"
  assume_role_policy = data.aws_iam_policy_document.assume.json
}

resource "aws_iam_role_policy" "s3_access" {
  role   = aws_iam_role.app.id
  policy = data.aws_iam_policy_document.s3.json
}

resource "aws_instance" "worker" {
  ami                  = "ami-0c55b159cbfafe1f0"
  instance_type        = "t3.small"
  iam_instance_profile = aws_iam_instance_profile.app.name

  # The instance never references the policy, but it must exist
  # and be attached before the instance boots and runs its code.
  depends_on = [aws_iam_role_policy.s3_access]
}

depends_on takes a list of references to whole resources or modules (no attributes). Use it sparingly: overusing it serializes work that could run in parallel and makes the configuration brittle.

Reference vs. depends_on at a glance

AspectImplicit (reference)Explicit (depends_on)
How it’s createdReading a.b.attr in another resourceListing the resource in depends_on
Best forNormal data flow between resourcesHidden side-effect dependencies
Self-documentingYes — the value usage shows whyNo — intent must be inferred or commented
Maintenance riskLow — updates with your referencesHigher — easy to leave stale
Affects parallelismOnly where data genuinely flowsCan over-serialize if misused

Seeing the order Terraform chose

The plan output reflects the derived order, and you can inspect the raw graph directly. Both commands work identically in OpenTofu (tofu graph, tofu plan).

terraform graph | dot -Tsvg > graph.svg
terraform plan

Output:

Terraform will perform the following actions:

  # aws_vpc.main will be created
  + resource "aws_vpc" "main" {
      + cidr_block = "10.0.0.0/16"
      + id         = (known after apply)
    }

  # aws_subnet.app will be created
  + resource "aws_subnet" "app" {
      + cidr_block = "10.0.1.0/24"
      + id         = (known after apply)
      + vpc_id     = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

The (known after apply) placeholders are the visible evidence of an ordering constraint: vpc_id cannot be filled in until aws_vpc.main exists, so the subnet is guaranteed to be created afterward.

Cycles and how to avoid them

Because ordering comes from edges, two resources that reference each other create a cycle, which Terraform cannot resolve.

Output:

Error: Cycle: aws_security_group.a, aws_security_group.b

Break cycles by splitting the mutual reference into a separate resource — for security groups, use standalone aws_security_group_rule (or aws_vpc_security_group_ingress_rule) resources instead of inline rules that reference each other.

Best Practices

  • Express ordering by referencing attributes, not by reordering blocks or files — block position is irrelevant.
  • Reserve depends_on for genuine hidden dependencies, and add a comment explaining why it’s needed.
  • Keep depends_on lists minimal; every extra entry can reduce parallelism and add maintenance risk.
  • Run terraform graph when an apply order surprises you, rather than guessing.
  • Split mutually-referencing resources (like paired security group rules) into standalone resources to avoid cycles.
  • Trust reverse-order destroys: design with the knowledge that dependents are removed before their dependencies.
Last updated June 14, 2026
Was this helpful?