Resources, Providers & the Dependency Graph
Providers turn HCL into API calls, and resources are the nodes in the graph. This page covers aliases, implicit edges, lifecycle, and why for_each beats count when a set changes shape.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
Where does an implicit edge come from?
Answer
An interpolation such as subnet_id = aws_subnet.a.id. Terraform creates the subnet before the instance.
L2
When is depends_on the right tool?
Answer
When the API coupling is invisible to references, such as IAM propagation. Prefer a real attribute edge when one exists.
L3
Why does removing item 0 from a count list replace resources?
Answer
count addresses are integers. The list shifts and Terraform sees new addresses.
L4
What does create_before_destroy change?
Answer
The replacement is created first, then the old object is destroyed, when names and attachments allow it.
L5
What is a provider source address?
Answer
Coordinates such as registry.terraform.io/hashicorp/aws, so CI does not fetch an unexpected fork.
L6
How do you move an address without touching the cloud object?
Answer
terraform state mv, then a plan that shows no destroy and no create.
L7
When do you split one resource into two?
Answer
When unrelated changes keep replacing a load-bearing object. Example: a security group plus separate rule resources.
Failure modes
count on a list you will reorder
Index 0 changes identity and Terraform replaces the whole tail of the list.
ignore_changes with no owner
Drift becomes permanent because every plan is taught to look away.
A module that configures its own provider
The root alias is ignored and resources land in the wrong region or account.
A cycle
The graph cannot be ordered. Apply fails before any API call, which is the good outcome.
Casual replace of a data store
A force-new attribute destroys the database or volume along with the compute wrapped around it.
Misconceptions
depends_on replaces the need for references.
References carry data and an edge. depends_on adds an edge without data flow.
Data sources are how a team should create shared infrastructure.
A data source reads. If your team should own the object, it is a resource in a root you name.
Parallelism reorders the graph.
It only runs ready nodes side by side. Edges still win.
Interviewer traps
Saying a security-group rule change always replaces the instance.
Check force-new on the schema. Prefer separate rule resources so the instance address stays stable.
Walking a blue-green rollout design when the question was a replace.
Say whether identity and data die with the resource. Point workload rollouts at the Kubernetes page.
Design scenario
Same prompt for every reader.
Requirements
Pinned provider, committed lockfile, for_each keyed by name, and lifecycle meta-arguments that each have an owner comment. No cycles.
Traffic / scale
One network root applied a few times a week. App roots read subnet IDs from it.
Latency
Plans stay reviewable. Parallelism may drop if the provider rate-limits.
Consistency
A plan after state mv shows zero destroys. A rule edit does not replace the instance.
Availability
create_before_destroy is used only when the name and attachments can overlap. Data stores use prevent_destroy plus backups, not a lifecycle flag alone.
Failure assumptions
- An inline security-group rule is force-new for the parent.
- IAM is eventually consistent and a hidden edge is required.
- A provider upgrade renames an attribute.
Constraints
- Do not use count for a set of named objects.
- Do not bury ignore_changes without a comment.
- Pass provider aliases into modules on purpose.
Prompt
A root creates a VPC, subnets, a security group with inline rules, and instances. Changing one ingress rule shows a replace on the instances. The provider major version is also a year behind.
API
Which interpolation creates the edge from subnet to instance?
Data
What does state mv change, and what does it leave in the cloud?
Architecture
Where do the provider lockfile and the alias for the DR region sit?
A security group rule changes every sprint and the instance must stay
Prefer
A stable group plus separate rule resources
The instance references the group ID. Rule addresses come and go without replacing the instance.
- The graph shows a real edge from group to rule.
- for_each keys are rule names, not list indexes.
- The instance lifecycle does not need ignore_changes to survive a rule edit.
Alternative
Inline rules on the same resource as the instance
A force-new attribute on the parent replaces the compute you meant to keep.
- The plan says replace and the data volume is in the blast radius.
- count indexes shift when someone deletes the first rule.
- Reviewers cannot see which edge caused the replace.
From an interpolation to a safe replace
Create order follows edges. A replace is a destroy plus a create unless lifecycle says otherwise.
- 1
Read the reference
subnet_id = aws_subnet.a.id is an edge. The subnet exists before the instance. - 2
Add depends_on only for hidden couplings
IAM propagation and similar delays. A reference is still better when the attribute exists. - 3
Check force-new before apply
user_data, availability zone, and some name fields replace. Ask whether data dies with the object. - 4
Refactor addresses with state mv
Move count to for_each or into a module, then plan until the diff is empty of creates and destroys.
Overview
Providers translate HCL into cloud API calls. Resources are the units in the graph. Implicit versus explicit edges, create-before-destroy, and provider aliases are what separate a working example from an apply you can predict.
Providers, versions, and aliases
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "dr"
region = "us-west-2"
}Pin the provider in the root. Run terraform providers lock and commit .terraform.lock.hcl so CI on Linux does not fetch a different binary than a laptop on macOS. In CI, prefer OIDC or role assumption over static keys. The blast-radius page owns the role split.
Pass aliases on purpose: providers = { aws = aws.dr }. A module that silently uses the default provider is a bug class. Do not put provider blocks inside shared child modules.
Implicit edges
resource "aws_subnet" "a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "app" {
subnet_id = aws_subnet.a.id
}Create walks VPC, then subnet, then instance. Destroy reverses that when it is safe.
Flow
- 1
1. aws_vpc.main
- next2. aws_subnet.a
- next3. aws_security_group
- 2
2. aws_subnet.a
- next4. aws_instance.app
- 3
3. aws_security_group
- 4
4. aws_instance.app
Lesson map
Resources, Providers & the Dependency Graph
Providers turn HCL into API calls, and resources are the nodes in the graph. This page covers aliases, implicit edges, lifecycle, and why for_each beats count when a set changes shape.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB vpc["1. aws_vpc.main"] subnet["2. aws_subnet.a"] sg["3. aws_security_group"] inst["4. aws_instance.app"] vpc -->|1. aws_vpc.main to 2. aws_subnet.a| subnet vpc -->|1. aws_vpc.main to 3. aws_security_group| sg subnet -->|2. aws_subnet.a to 4. aws_instance.app| inst
terraform graph can draw this. Huge states become hairballs. Smaller roots beat a prettier picture. How those roots share outputs is the state page.
Explicit depends_on
Use it when the coupling is invisible: IAM is not assumable yet, or a function needs a log group that its arguments do not reference. Overuse hides real edges and makes refactors harder.
Lifecycle
| Meta-argument | Use it when |
|---|---|
create_before_destroy | A replacement can exist beside the old object long enough to cut over |
prevent_destroy | The object is irreplaceable data. This is a guard rail, not a backup |
ignore_changes | Another system owns a field. Every ignore needs an owner comment |
replace_triggered_by | A related resource change must force a new object (Terraform 1.2 and later) |
ignore_changes is how drift becomes permanent if you are sloppy.
count, for_each, and dynamic blocks
Prefer for_each for a set of named objects. Removing item 0 from a count list renumbers every later address and forces replacements. Key maps by name or availability zone, not by list index. dynamic blocks are fine when they stay readable. If they do not, move the shape into a module.
Replacement
When a force-new attribute changes, Terraform plans destroy plus create, or create-before-destroy. Ask:
- Is the name or ARN load-bearing for clients?
- Is there data on the object, such as a volume or a database, that dies with it?
- Can a higher layer, such as an autoscaling group or a Kubernetes rollout, absorb the cutover?
If data dies with the object, do not casually accept the replace. Split the data resource, snapshot first, or document an ignore_changes migration. Workload rollout patterns live on rolling, blue-green, and canary. This page stops at the resource address.
Decisions
- ?
1. Unrelated edits replace it?
- yes2. Split core and leaf
- no3. Keep one address
- 2
2. Split core and leaf
- next4. Group plus rule resources
- next5. Role plus policy attachments
- 3
3. Keep one address
- 4
4. Group plus rule resources
- 5
5. Role plus policy attachments
Data sources versus resources
| Data source | Resource | |
|---|---|---|
| Purpose | Read an existing object | Own create, update, and delete |
| On each plan | Read again | Tracked in state |
| Smell | "Finding" an object your team should own | A second copy of a shared VPC |
A shared VPC lookup is a fair data source. A second VPC, because the data source felt hard, is not.
Provider behavior seniors mention
- Eventual consistency. A new IAM role may not be assumable on the next call. Providers retry. Sometimes you still need a hidden edge.
- Force-new attributes. Read the schema.
user_dataor the availability zone may replace an instance. - Default tags. Set them on the provider. Do not fight them with ad hoc resource tags.
- Parallelism. The default of 10 speeds independent nodes and can trip rate limits. Lower it for a flaky provider. Ordering still follows the DAG.
One root versus layered roots
From the graph's point of view, network then platform then app keeps each DAG reviewable and lets teams apply on different cadences. The cost is an output contract, which the state page already constrained to narrow IDs.
Cycles
Press Run. Snippets must be self-contained — no network, files, or native modules.
Press Run. Snippets must be self-contained — no network, files, or native modules.
The count sketch shows the failure mode: drop the first name and the integer addresses still look like a tail was removed. for_each removes the key you named.
Graphs versus imperative scripts
| Imperative script | Terraform graph |
|---|---|
| You encode order by hand | The graph derives order |
| Drift is a custom check | The plan shows drift |
| One-off surgery is natural | Surgery needs import or state mv |
| Right for an emergency | Right for steady state |
Keep a break-glass script. Backfill into Terraform. Import versus recreate is the plan page.
Interview Q&A
Why did changing a security group rule replace my instance?
Answer
A force-new attribute, or inline rules stored on a parent that the instance depends on in a way that recreates it. Prefer independent rule resources so the instance address stays put.
depends_on versus a reference. Which one orders the apply?
Answer
Both add edges. A reference also passes data. depends_on is the edge you add when there is no attribute to interpolate.
How do you upgrade a provider major version?
Answer
Read the changelog. Plan in a non-prod root. Watch for renames and force-new attributes. Update module constraints. Commit the new lockfile. Canary prod.
What is a provider source address?
Answer
Coordinates such as registry.terraform.io/hashicorp/aws. They stop CI from resolving a name collision to the wrong fork.
What does terraform state mv do?
Answer
It renames an address in state and does not call the cloud. Use it for count to for_each and for moves into a module. The following plan should show no destroy and no create.
Why commit the provider lockfile?
Answer
It pins checksums per platform. Without it, a Linux CI agent can run a different provider build than the Mac where the plan was sketched.
When is a data source the wrong tool?
Answer
When your team should own the object's lifecycle. Reading a shared VPC is fine. Creating a private copy because the lookup was annoying splits the source of truth.
What does parallelism actually parallelize?
Answer
Nodes whose edges are already satisfied. It does not skip an edge. Lower it when the API rate-limits or IAM races. Raise it only for a wide graph of independent objects.
Pitfalls
- Provider blocks inside reusable modules.
ignore_changescopied from a snippet with no comment and no owner.- A mega-resource that mixes a stable core with a volatile leaf.
- Raising parallelism to hide a cycle. Cycles still fail the graph.
- Drawing a giant SVG instead of splitting the root.
An instance subnet ID references aws_subnet.a.id, and the instance also depends_on an IAM role it does not interpolate. Say which edge is implicit, which is explicit, what create order you expect, and what you would move with state mv if the instance later shifts from count to for_each.