TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

The Dependency Graph and the Inside of the State File

Continue in TT Lab

In one line

Terraform moves not in the order written in the file but in the order of the graph that references create. That graph remains like a fossil in the state file's dependencies, and tells the deletion order even after a resource has disappeared from the code.

Why this was needed

In a shell script, a person decides the order. With five resources you can read from top to bottom, but with thirty, every time you insert a new resource a person has to re-decide "where should this come after?" Moreover, unrelated resources are also lined up and created one at a time, so the time to create them simply adds up.

A declarative tool turns this problem around. A person writes only the relationships and not the order. When creating A, if you use the value of B, that itself is the declaration "B comes first." The tool gathers these relationships to build a directed graph and topologically sorts it, processing unrelated things at the same time. When deleting, it walks the same graph backward.

How it works

There are two ways to create a dependency.

Method How it arises When to use
Implicit dependency Automatically, when you reference another resource's attribute in an expression The default. When the value is actually needed
Explicit dependency By writing depends_on = [주소] yourself (the placeholder is the address) When there is no reference but there must be an order

Implicit is enough for most cases. depends_on is needed when there is a side effect that does not show up as a value. For example, resource creation fails until an IAM policy is attached, yet the code does not use that policy's value.

If you attach depends_on out of habit, the graph gets thick. Then there are two losses. First, things that could be processed at the same time line up and the execution time grows. Second, when the earlier resource is recreated, the range of later resources recreated along with it widens.

Let us also look at the state file side. terraform state list is the fastest way to check, printing only the registered addresses, one per line. When you need to handle it with a machine, use terraform show -json. The resource list in this output is under values.root_module.resources and each entry has an address, making it good to work with using jq. If you read the state file itself directly, you can see serial, lineage, version, and resources.

The .terraform.lock.hcl is also worth reading at this point. Inside are blocks such as provider "registry.opentofu.org/hashicorp/local", the confirmed version, and the hashes beginning with h1: or zh:. The version means "what we decided to use," and the hash means "whether the thing we actually downloaded is that one."

What you see in the field

First, the depends_on domino. Once an ordering problem occurs, people start attaching depends_on here and there. A few months later, you fix just one database parameter and the plan shows "12 recreations." The price of thickening the graph is always billed later.

Second, the state knows the deletion order, not the code. When you delete a resource from the code, that resource's relationships vanish from the code. Yet the tool can still delete in the correct reverse order because the relationships remain in the state's dependencies. It is one of the reasons editing the state by hand is dangerous.

Third, drift is not a special feature. If you fix a managed resource outside the code, the next plan finds that difference in the query stage and tries to revert it. This is where the accident comes from in which a setting fixed in a hurry in the console during incident response silently disappears in the next deployment. An urgent change must always be brought back into the code.

When a dependency exists but Terraform does not know

Terraform reads order from references. If you use another resource's attribute, as in aws_instance.web.subnet_id, it knows on its own that the resource must be created first. The problem is a dependency that exists without a reference.

resource "aws_iam_role_policy" "app" { ... }

resource "aws_instance" "app" {
  # 정책이 붙기 전에 인스턴스가 떠서 부팅 스크립트가 실패한다
  depends_on = [aws_iam_role_policy.app]
}

Use depends_on only in such cases. If you attach it out of habit, it lines up even things that could be created in parallel, slowing the apply and making the graph invisible to human eyes. If it can be expressed with a reference, a reference is always better.

The order shown in the plan is not the execution order. The output of terraform plan is close to alphabetical order. The actual order is decided by the graph.

terraform graph | dot -Tsvg > graph.svg

Resource replacement sets off a chain. If an attribute with # forces replacement appears in the plan, that resource is deleted and recreated. Things that reference it change too, so the situation arises where you fixed one line and twelve things show up in the plan. Turning on create_before_destroy then creates the new one first and deletes the old one, but for a resource whose name must be globally unique, it actually collides. This is why you attach random_id to the name or use name_prefix.

lifecycle {
  create_before_destroy = true
  ignore_changes        = [tags["LastModified"]]
}

Write the target of ignore_changes narrowly. If you ignore it wholesale, that resource effectively goes out of management. Write only the attributes where you know who changes them and why, like a single tag attached from outside.

Instead of count, use for_each. A list made with count has numbers as addresses. If you delete one in the middle, the ones after it shift forward by one and perfectly healthy resources all get replaced. The address of a for_each is a key, so only what you deleted disappears.

What you will do in the next lab

Stacking resources in /root/tf/state, you confirm in the state that dependencies arise from references alone, and where there is no reference you try attaching depends_on. You extract the list of state addresses and the JSON representation, and make a metadata file holding serial, lineage, and the resource count. Then you delete a managed file by hand and see drift caught in the plan, and read the provider version from the lock file. Finally, you build a 3-stage dependency chain and record the order.