Component · State
*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.
*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
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
}
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
*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=="
}
]
}
]
}
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.).
When a resource has lifecycle { create_before_destroy = true }
and its plan contains a replacement, the apply walk:
DeposedKey.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
}