Component · State

State Management

*states.State is the ground truth of managed infrastructure. SyncState wraps it with a sync.RWMutex for concurrent graph walk access. Backends persist it between runs.

State struct: states/state.go:27 SyncState: states/sync.go:36
State object hierarchy
*states.State
  │
  ├── Modules: map[string]*Module            keyed by module instance address
  │     e.g. "" (root), "module.vpc[0]", "module.network.module.subnets"
  │
  ├── RootOutputValues: map[string]*OutputValue
  │
  └── CheckResults: *CheckResults            results of check {} blocks

*states.Module
  │
  ├── Addr: addrs.ModuleInstance
  │
  ├── Resources: map[string]*Resource        keyed by "type.name"
  │     e.g. "aws_instance.web"
  │
  ├── OutputValues: map[string]cty.Value
  │
  └── LocalValues: map[string]cty.Value

*states.Resource
  │
  ├── Addr: addrs.Resource
  │
  ├── EachMode: EachList | EachMap | NoEach  set by count / for_each
  │
  └── Instances: map[addrs.InstanceKey]*ResourceInstance
        Keys: IntKey(0), IntKey(1)…  for count
              StringKey("us-east-1")… for for_each

*states.ResourceInstance
  │
  ├── Current: *ResourceInstanceObjectSrc   nil if only deposed instances
  │
  └── Deposed: map[DeposedKey]*ResourceInstanceObjectSrc
        Non-empty only during a create-before-destroy replacement

*states.ResourceInstanceObjectSrc
  │
  ├── SchemaVersion: uint64                 for UpgradeResourceState migrations
  ├── AttrsJSON: []byte                     JSON-encoded attribute values
  ├── AttrSensitivePaths: []cty.Path        paths of sensitive attributes
  ├── Private: []byte                       opaque provider metadata
  ├── Status: ObjectReady | ObjectTainted
  └── Dependencies: []addrs.ConfigResource  other resources this depends on
states.State struct
internal/states/state.go:27 GitHub
// State is the top-level type for a Terraform state.
// NOT goroutine-safe — use SyncState for concurrent access.
type State struct {
    // Modules maps module instance addresses to their state.
    // The root module address is "".
    Modules map[string]*Module

    // RootOutputValues contains the root module's output values.
    RootOutputValues map[string]*OutputValue

    // CheckResults collects the results of check {} block assertions.
    CheckResults *CheckResults
}

type Module struct {
    Addr         addrs.ModuleInstance
    Resources    map[string]*Resource
    OutputValues map[string]*OutputValue
    LocalValues  map[string]cty.Value
}

type Resource struct {
    Addr      addrs.AbsResource
    Instances map[addrs.InstanceKey]*ResourceInstance
    EachMode  EachMode  // NoEach, EachList, EachMap
    ProviderConfig addrs.AbsProviderConfig
}

type ResourceInstance struct {
    Current *ResourceInstanceObjectSrc
    Deposed map[DeposedKey]*ResourceInstanceObjectSrc
}

type ResourceInstanceObjectSrc struct {
    SchemaVersion       uint64
    AttrsJSON           []byte              // JSON-encoded cty.Value
    AttrSensitivePaths  []cty.Path
    Private             []byte
    Status              ObjectStatus
    Dependencies        []addrs.ConfigResource
    CreateBeforeDestroy bool
}
SyncState — thread-safe wrapper

The graph walk executes up to 10 nodes concurrently. All state reads and writes must go through SyncState to prevent data races. It uses sync.RWMutex: writes hold the exclusive lock; reads hold the shared lock.

internal/states/sync.go:36 GitHub
// SyncState provides a goroutine-safe wrapper around *State.
type SyncState struct {
    state *State
    lock  sync.RWMutex
}

// SetResourceInstanceCurrent is the primary write path during apply.
// Called by NodeApplyableResourceInstance.Execute() after a successful
// ApplyResourceChange call.
func (s *SyncState) SetResourceInstanceCurrent(
    addr    addrs.AbsResourceInstance,
    obj     *ResourceInstanceObjectSrc,
    provider addrs.AbsProviderConfig,
) {
    s.lock.Lock()
    defer s.lock.Unlock()
    // ... mutate s.state
}

// ResourceInstanceObject reads a single instance for the plan walk.
// Uses a read lock so multiple goroutines can read concurrently.
func (s *SyncState) ResourceInstanceObject(
    addr addrs.AbsResourceInstance,
    gen Generation,
) *ResourceInstanceObjectSrc {
    s.lock.RLock()
    defer s.lock.RUnlock()
    // ...
}

// ForgetResourceInstanceAll removes all instances of a resource.
// Called by NodeDestroyResourceInstance after a successful destroy.
func (s *SyncState) ForgetResourceInstanceAll(addr addrs.AbsResourceInstance)

// Close returns the underlying *State, removing the SyncState wrapper.
// Called at the end of Apply to hand the final state to the caller.
func (s *SyncState) Close() *State
State serialization — terraform.tfstate

*states.State is serialized to JSON by statefile.Write in internal/states/statefile/. The format version is incremented when breaking changes are made; older formats are upgraded on read via migration functions.

terraform.tfstate (example, v4 format)
{
  "version": 4,
  "terraform_version": "1.9.0",
  "serial": 42,
  "lineage": "abc123...",
  "outputs": {
    "vpc_id": { "value": "vpc-0abc", "type": "string" }
  },
  "resources": [
    {
      "module": "module.network",
      "mode": "managed",
      "type": "aws_vpc",
      "name": "main",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id":         "vpc-0abc123",
            "cidr_block": "10.0.0.0/16",
            "tags":       { "Name": "main" }
          },
          "sensitive_attributes": [],
          "private": "base64encodedproviderdata=="
        }
      ]
    }
  ]
}

Serial and lineage

  • lineage — a UUID generated when the state is first created. Two state files with different lineages are considered unrelated and Terraform will refuse to merge them.
  • serial — monotonically increasing integer. The backend increments it on every write and uses it for optimistic concurrency control (a write with a stale serial is rejected).
State lifecycle across plan and apply
PLAN PHASE
  Backend.StateMgr(workspace).GetState()
    └─► *states.State  (prevRunState)
          │
          ├── Passed to Context.Plan() as prevRunState
          │
          ├── REFRESH WALK:
          │   SyncState wraps a copy of prevRunState
          │   providers.ReadResource() → update each resource's attrs
          │   → refreshedState  (stored as plan.PriorState)
          │
          └── PLAN WALK:
              SyncState holds refreshedState (read-only during plan)
              Changes are collected in plans.ChangesSync (separate from state)
              → *plans.Plan.Changes  (delta, not new state)

PLAN FILE: embeds prevRunState as "tfstate"

APPLY PHASE
  planfile.ReadStateFile()  →  prevRunState from plan time
    │
    └── Passed to Context.Apply()
          SyncState starts with copy of prevRunState
          │
          ├── For each resource in plan.Changes:
          │     ApplyResourceChange() → new cty.Value
          │     SyncState.SetResourceInstanceCurrent(addr, newObj)
          │
          └── SyncState.Close()  →  *states.State  (finalState)
                │
                └── statemgr.Full.WriteState(finalState)
                      statemgr.Full.PersistState()
                        └── Backend writes terraform.tfstate

State locking: Before writing state, the backend calls statemgr.Locker.Lock(). If another Terraform operation holds the lock, this blocks until it is released (or times out). The local backend uses a .terraform.tfstate.lock.info file; cloud backends use native locking (DynamoDB, Consul, etc.).

Deposed instances — create-before-destroy

When a resource has lifecycle { create_before_destroy = true } and its plan contains a replacement, the apply walk:

  1. Creates the new instance; stores it as Current.
  2. Moves the old instance to Deposed with a generated DeposedKey.
  3. Destroys the deposed instance; removes it from state.

If Terraform crashes between steps 2 and 3, the next apply finds the deposed instance in state and re-runs the destroy step. This ensures the old instance is eventually cleaned up without losing the new one.

internal/states/resource.go (DeposedKey) GitHub
// DeposedKey identifies an older object that has been replaced but not yet
// destroyed. It is a random 8-character hex string.
type DeposedKey string

// NotDeposed is the zero value; indicates an instance is not deposed.
const NotDeposed = DeposedKey("")

// NewDeposedKey generates a random DeposedKey for a replacement.
func NewDeposedKey() DeposedKey {
    // generates 4 random bytes → 8-char hex string
}

type ResourceInstance struct {
    // Current is the live instance; nil during a pending destroy.
    Current *ResourceInstanceObjectSrc

    // Deposed holds replaced-but-not-yet-destroyed instances.
    // Normally empty; non-empty only between create and destroy
    // steps of a create_before_destroy replacement.
    Deposed map[DeposedKey]*ResourceInstanceObjectSrc
}