TerraformII · Terraform ArchitectureProduction Terraform
State and the Resource Model
What you'll learn
- Describe what the state file records and what it deliberately omits
- Explain the conceptual resource model: type, name, attributes, dependencies, lifecycle
- Apply resource addresses correctly when reading state
- Recognise how Terraform decides which API calls to make from the diff between configuration and state
- Use terraform state list, show, and mv to inspect state without mutating it
Prerequisites
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
State is the deepest production topic in this course, and the resource model is the conceptual framework that makes state useful. The state file records what Terraform believes exists in the real world. The resource model is the schema-driven view that lets Terraform compare the configuration to the state and decide which provider API calls to make. If you do not understand state and the resource model, every plan output is a surprise.
What the state file records
A state file is a single JSON document. The top-level structure:
{
"version": 4,
"terraform_version": "1.9.8",
"serial": 17,
"lineage": "a7c3f0e2-9c5d-4b8e-b1f0-...",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0abc123def456789",
"ami": "ami-0e1bed4f",
"instance_type": "t3.medium",
"tags": {"Name": "web-01"}
},
"dependencies": [
"aws_subnet.public",
"aws_security_group.web"
]
}
]
}
]
}
The fields that matter operationally:
| Field | What it holds | Why it matters |
|---|---|---|
version | State schema version | Determines which Terraform version can read the file |
terraform_version | The version that wrote it | Detects stale state |
serial | Monotonic counter | Bumped on every write; ordering signal |
lineage | UUID | Identifies the lineage; rolled on terraform state mutations |
outputs | Last computed values | Available via terraform output and downstream modules |
resources[].instances[].attributes | Attributes the provider returned after the last refresh | What Terraform believed was true at the last refresh |
resources[].instances[].dependencies | Computed dependencies | Cross-reference for ordering |
The state does not contain:
- The user’s
.tfconfiguration. - The variable values that resolved during the last apply.
- Outputs that have not been computed.
- A history of past plans or applies.
- Every real-world attribute the provider could expose — only the subset the provider’s schema declares.
The state is a snapshot, not a journal. It records what Terraform believed at the last refresh; it does not record the history of how Terraform arrived at that belief.
The conceptual resource model
Every resource in Terraform is described by the same conceptual model:
+---------------------+
| Type | e.g. "aws_instance"
+---------------------+
| Name (local label) | e.g. "web"
+---------------------+
| Module path | e.g. module.network.aws_subnet.public
+---------------------+
| Provider config | e.g. provider = aws.west
+---------------------+
| Configuration | The arguments in the .tf file
| block |
+---------------------+
| Attributes | What the provider returns after refresh
+---------------------+
| Dependencies | Implicit + explicit
+---------------------+
| Lifecycle meta | create_before_destroy, prevent_destroy,
| | ignore_changes, precondition, postcondition
+---------------------+
| Index / key | count index or for_each key
+---------------------+
The model is what Terraform uses to decide whether a resource should be created, updated, destroyed, or replaced. The decision is per-resource; the graph walker visits resources in topological order, but the action is decided by comparing the configuration to the refreshed state for each resource individually.
Resource addresses
A resource address is a fully qualified path to a single resource or resource instance:
aws_instance.web
module.network.aws_subnet.public[0]
module.app["api"].aws_iam_role.lambda[0]
The format is:
[module.<name>[...].]<type>.<name>[<index_or_key>]
- Module paths are dot-separated and appear as
module.<name>. - Nested modules repeat the
module.<name>pattern. countresources use numeric indices in[0],[1], etc.for_eachresources use map keys in[...](with quoting if the key is a string).
Resource addresses are how you refer to a resource in:
terraform state show <address>terraform state mv <old> <new>-target=<address>terraform import <address> <id>
Getting the address wrong produces an error that is sometimes
opaque. A common production mistake: terraform state mv aws_instance.web module.network.aws_instance.web (the target
address is missing the resource name) returns
Error: Invalid address.
Schema-driven view of resources
The provider defines the schema for each resource type. The schema is a typed description of:
- The arguments the resource accepts.
- The attributes the resource returns.
- Which attributes are
computed(set by the provider, not by the user). - Which attributes are
sensitive(masked in plan output). - Which attribute changes are in-place vs force replacement.
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
tags = {
Name = "web-01"
}
}
For this block, the AWS provider’s schema declares that:
amiis a required string argument.instance_typeis an optional string argument with a default oft3.micro.tagsis an optional map of strings.idis a computed attribute (returned by the provider after creation).- Changing
amiforces a replacement. - Changing
instance_typeis in-place. - Changing
tagsis in-place.
The provider decides which changes are replacement. The core makes the decision about ordering. The state records what happened.
How Core decides which API calls to make
The decision tree for each resource:
1. Refresh: ask the provider for current real-world attributes.
Update state with what was returned.
2. Compare desired config to refreshed state:
- If resource is in state and config has it: compute diff.
- If resource is in state and config omits it: destroy.
- If resource is not in state and config has it: create.
- If resource is not in state and config omits it: no-op.
3. Send the diff to the provider's PlanResourceChange:
- Provider returns the action: no-op, create, read, update,
delete, or a sequence (e.g. delete then create for a
replacement).
4. Core records the planned action in the plan output.
5. On apply, Core sends the planned action to the provider's
ApplyResourceChange:
- Provider performs the API calls.
- Provider returns the new attributes.
- Core writes the new attributes to state.
The provider makes the decision about which API calls to make
(the schema declares what is in-place vs replacement; the
provider’s PlanResourceChange decides the action). The core
makes the decision about order (the graph walker visits
resources in topological order).
Inspecting state
# List every resource in state
terraform state list
# List resources within a module
terraform state list module.network
# Show the details of one resource (read-only)
terraform state show aws_instance.web
# Show the full state in JSON (for tooling)
terraform show -json
# Move a resource in state (e.g. after refactoring module paths)
terraform state mv aws_instance.web module.app.aws_instance.web
state list and state show are read-only and safe to run in
production. state mv mutates state and should be used only
when the configuration has been refactored and the resource’s
address has changed.
Lifecycle meta-arguments
The lifecycle block is part of the resource model:
resource "aws_db_instance" "primary" {
engine = "postgres"
instance_class = "db.t3.medium"
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags]
}
}
The lifecycle block declares:
create_before_destroy: invert destroy order; create replacement first.prevent_destroy: refuse to destroy the resource; the plan will fail if destruction is the proposed action.ignore_changes: ignore changes to listed attributes when computing the diff; useful for attributes that change outside Terraform.precondition/postcondition: assertions that must hold before apply and after apply respectively.
Lifecycle meta-arguments are part of the configuration but they do not modify the schema. They modify how Core decides the action for the resource.
Production failure modes
| # | Failure mode | Observable symptom | Recovery |
|---|---|---|---|
| 1 | State references a resource that no longer exists in real world | Refresh returns Error: Resource not found; plan fails | Remove from state with terraform state rm <address> or restore state from backup |
| 2 | Resource address ambiguity after a refactor | terraform plan fails with Invalid address | Use terraform state list to find the current address; terraform state mv to align with the new configuration |
| 3 | Provider schema mismatch (state written by one provider version, another expects different schema) | Error: Provider produced inconsistent final plan | Upgrade the provider version intentionally; do not bypass the lock file |
| 4 | Lifecycle default vs create_before_destroy | Stateful resource is destroyed before replacement is ready | Add lifecycle.create_before_destroy = true; use prevent_destroy for irreversible resources |
| 5 | Data source fails to read | Configuration cannot resolve; terraform plan fails before apply | Investigate the data source (typically credentials, region, or permissions); fix the cause; re-plan |
| 6 | Sensitive attribute stored in state and printed in plan output | Credentials visible in CI logs | Mark attributes sensitive = true; archive plan output with access controls |
Security implications
- State has real IDs and may have sensitive data. A database
password is stored in state unless marked
sensitive. An API key returned by a data source is stored in state. - State is read on every command. Anyone with read access to the state can read the topology and the secrets. Anyone with write access can rewrite Terraform’s model of the world.
- State is not in Git. State in Git leaks the topology through the commit history, exposes secrets, and makes concurrent access unsafe.
terraform show -jsonis as sensitive as state. Treat the JSON output with the same access controls as the state file.
Performance implications
- State size affects plan time. A 5,000-resource state deserialises and refreshes in O(N) provider calls. Plan time is roughly N provider calls / parallelism.
- Schema size affects plan time per resource. A resource with 100 attributes takes longer to diff than a resource with 5. Most providers keep attribute counts reasonable; some do not.
terraform state listis fast even on large states. The JSON parse is cheap; the listing is a tree walk.
Production guidance
- Inspect state with
terraform state listandterraform state showregularly. These are read-only and safe to run in production. - Use
movedblocks for refactors. They update state addresses in-place without recreating resources. - Treat
prevent_destroyas a last-line guard. It stops accidental destruction but it does not stop forced destruction viaterraform state rm. - Mark sensitive attributes explicitly. The default
behaviour prints the value in plan output. Mark
sensitive = truefor any attribute that holds a secret. - Do not write to state by hand. The state format is documented but the field semantics are not. A bad edit can corrupt state.
Verification
- What does the state file record, and what does it deliberately omit?
- What are the components of the conceptual resource model?
- How does Terraform decide whether a resource should be created, updated, destroyed, or replaced?
- What is the difference between
terraform state listandterraform state show? - When does the resource address change, and how do you align state with the new address?
Knowledge check · 7 questions
Q1. What is the role of the state file in Terraform?
Q2. What decides whether a configuration change is an in-place update versus a destroy-create replacement?
Q3. The state file is a snapshot of the last refresh rather than a journal, so it holds no history of the plans and applies that came before.
Q4. Which terraform state command is safe to run in production without risk of mutating state?
Q5. Which of the following are parts of the conceptual resource model? (Select all that apply.)
Q6. What is the purpose of lifecycle.prevent_destroy?
Q7. A team refactors a module to move a resource from one address to another. After the configuration change, terraform plan proposes to destroy the old resource and create a new one. What is the recommended fix?
Passing score: 75%. Answers are checked in this browser.