Skip to main content
RunBook Academy

TerraformII · Terraform ArchitectureProduction Terraform

State and the Resource Model

Foundation⏱ ~14 minbash

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

Not yet marked complete on this device.

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:

FieldWhat it holdsWhy it matters
versionState schema versionDetermines which Terraform version can read the file
terraform_versionThe version that wrote itDetects stale state
serialMonotonic counterBumped on every write; ordering signal
lineageUUIDIdentifies the lineage; rolled on terraform state mutations
outputsLast computed valuesAvailable via terraform output and downstream modules
resources[].instances[].attributesAttributes the provider returned after the last refreshWhat Terraform believed was true at the last refresh
resources[].instances[].dependenciesComputed dependenciesCross-reference for ordering

The state does not contain:

  • The user’s .tf configuration.
  • 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.
  • count resources use numeric indices in [0], [1], etc.
  • for_each resources 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:

  • ami is a required string argument.
  • instance_type is an optional string argument with a default of t3.micro.
  • tags is an optional map of strings.
  • id is a computed attribute (returned by the provider after creation).
  • Changing ami forces a replacement.
  • Changing instance_type is in-place.
  • Changing tags is 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 modeObservable symptomRecovery
1State references a resource that no longer exists in real worldRefresh returns Error: Resource not found; plan failsRemove from state with terraform state rm <address> or restore state from backup
2Resource address ambiguity after a refactorterraform plan fails with Invalid addressUse terraform state list to find the current address; terraform state mv to align with the new configuration
3Provider schema mismatch (state written by one provider version, another expects different schema)Error: Provider produced inconsistent final planUpgrade the provider version intentionally; do not bypass the lock file
4Lifecycle default vs create_before_destroyStateful resource is destroyed before replacement is readyAdd lifecycle.create_before_destroy = true; use prevent_destroy for irreversible resources
5Data source fails to readConfiguration cannot resolve; terraform plan fails before applyInvestigate the data source (typically credentials, region, or permissions); fix the cause; re-plan
6Sensitive attribute stored in state and printed in plan outputCredentials visible in CI logsMark 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 -json is 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 list is fast even on large states. The JSON parse is cheap; the listing is a tree walk.

Production guidance

  1. Inspect state with terraform state list and terraform state show regularly. These are read-only and safe to run in production.
  2. Use moved blocks for refactors. They update state addresses in-place without recreating resources.
  3. Treat prevent_destroy as a last-line guard. It stops accidental destruction but it does not stop forced destruction via terraform state rm.
  4. Mark sensitive attributes explicitly. The default behaviour prints the value in plan output. Mark sensitive = true for any attribute that holds a secret.
  5. 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 list and terraform state show?
  • When does the resource address change, and how do you align state with the new address?

Knowledge check · 7 questions

  1. Q1. What is the role of the state file in Terraform?

  2. Q2. What decides whether a configuration change is an in-place update versus a destroy-create replacement?

  3. 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.

  4. Q4. Which terraform state command is safe to run in production without risk of mutating state?

  5. Q5. Which of the following are parts of the conceptual resource model? (Select all that apply.)

  6. Q6. What is the purpose of lifecycle.prevent_destroy?

  7. 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.