Documentation Index Fetch the complete documentation index at: https://mintlify.com/hashicorp/terraform/llms.txt
Use this file to discover all available pages before exploring further.
This document describes the complete lifecycle of a Terraform resource instance as it flows through validation, planning, and applying phases.
Lifecycle Overview
A resource instance moves through several provider method calls:
State Values Through Lifecycle
Different state representations flow through the lifecycle:
Configuration
Prior State
Proposed New State
Planned State
New State
User-written values from .tf files.resource "aws_instance" "example" {
ami = "ami-123456"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
}
Null for unspecified optional attributes
May contain unknown values (from other resources)
Type-converted to match schema
Provider’s representation of current infrastructure.{
"ami" : "ami-123456" ,
"instance_type" : "t2.micro" ,
"id" : "i-1234567890abcdef0" ,
"tags" : { "Name" : "example-instance" },
"public_ip" : "52.1.2.3"
}
Result of last ApplyResourceChange or ReadResource
Fully known values only
May be stale (drift)
Terraform Core’s initial merge of config and prior state.{
"ami" : "ami-123456" , // From config
"instance_type" : "t2.micro" , // From config
"id" : "i-1234567890abcdef0" , // From prior state (computed)
"tags" : { "Name" : "example-instance" },
"public_ip" : "52.1.2.3" // From prior state (computed)
}
Config values override prior state
Computed values preserved from prior state
Provided to help provider planning
Provider’s prediction of final state.{
"ami" : "ami-123456" ,
"instance_type" : "t2.micro" ,
"id" : "<unknown>" , // Will be assigned
"tags" : { "Name" : "example-instance" },
"public_ip" : "<unknown>" // Will be assigned
}
Known config values preserved exactly
Unknown values for unpredictable attributes
Refinements constrain unknown values
Actual result after applying changes.{
"ami" : "ami-123456" ,
"instance_type" : "t2.micro" ,
"id" : "i-abcdef1234567890" ,
"tags" : { "Name" : "example-instance" },
"public_ip" : "52.5.6.7"
}
All values must be known
Known planned values unchanged
Unknown planned values resolved
See: docs/resource-instance-change-lifecycle.md
Phase 1: Previous Run State
When Terraform starts, it has the Previous Run State from the last operation.
UpgradeResourceState
Handles provider version upgrades:
Provider responsibilities:
Accept raw state data (any format)
Identify schema version
Transform to current schema
Do not detect external changes
Do not call external APIs
Provider Implementation
Example Transformation
func ( p * Provider ) UpgradeResourceState (
req UpgradeResourceStateRequest ,
) UpgradeResourceStateResponse {
// Decode old state
var oldState OldResourceStateV2
json . Unmarshal ( req . RawState . JSON , & oldState )
// Transform to current schema
newState := ResourceState {
ID : oldState . ID ,
Name : oldState . Name ,
NewField : "default" , // Add new field
RenamedOld : oldState . OldFieldName , // Rename
}
return UpgradeResourceStateResponse {
UpgradedState : newState ,
}
}
See: docs/resource-instance-change-lifecycle.md:224
ReadResource
Detects external changes (drift):
Provider must distinguish:
Same meaning, different representation // In state
{ "json_config" : "{ \" a \" : 1}" }
// From API
{ "json_config" : "{ \" a \" :1}" }
Action: Return state value unchanged (preserve user’s format)Rationale: Avoid spurious diffs from whitespace/formattingActual external change // In state
{ "instance_type" : "t2.micro" }
// From API
{ "instance_type" : "t2.small" }
Action: Return API value (report drift)Rationale: User needs to know infrastructure changed
Special cases:
Write-only attributes : Cannot detect drift (e.g., passwords)
Resource deleted : Return null state
Partial read failure : Return best-effort state + diagnostics
See: docs/resource-instance-change-lifecycle.md:252
Phase 2: Planning
Planning happens in two calls to PlanResourceChange.
First PlanResourceChange (Planning Phase)
Called during terraform plan:
Inputs:
prior_state: Current infrastructure state
proposed_new_state: Terraform Core’s merge suggestion
config: User configuration (may have unknown values)
Provider must:
Preserve non-null config values exactly
Use proposed_new_state as starting point
Mark unpredictable attributes as unknown
Return known values where possible
Indicate required replacements via requires_replace
Planning Logic
Example Plan Output
func ( p * Provider ) PlanResourceChange (
req PlanResourceChangeRequest ,
) PlanResourceChangeResponse {
planned := req . ProposedNewState . Copy ()
// Preserve config values
for attr , val := range req . Config . Attributes {
if ! val . IsNull () {
planned . Attributes [ attr ] = val
}
}
// Mark computed values as unknown if dependencies changed
if req . Config . AMI != req . PriorState . AMI {
planned . PublicIP = cty . UnknownVal ( cty . String )
planned . ID = cty . UnknownVal ( cty . String )
}
// Indicate if replacement needed
var requiresReplace [] cty . Path
if req . Config . AMI != req . PriorState . AMI {
requiresReplace = append ( requiresReplace ,
cty . GetAttrPath ( "ami" ))
}
return PlanResourceChangeResponse {
PlannedState : planned ,
RequiresReplace : requiresReplace ,
}
}
Constraints enforced by Terraform:
Non-null config values must appear unchanged in plan
Null optional+computed attributes may be set to any value
Planned known values cannot change in ApplyResourceChange
See: docs/resource-instance-change-lifecycle.md:134
Second PlanResourceChange (Apply Phase)
Called during terraform apply before applying:
Differences from first call:
All config values are known (dependencies applied)
Can refine previous unknowns to known values
Can replace unknowns with better unknowns (refinements)
Cannot change previously known values
Terraform enforces:
Known values from initial plan must be identical
Unknown values can become known or stay unknown
Unknown values can gain refinements
Example refinement:
// Initial Planned State
{
"url" : "<unknown string>"
}
// Final Planned State (after config becomes known)
{
"url" : "<unknown string with prefix 'https://'>"
}
See: docs/resource-instance-change-lifecycle.md:160
Phase 3: Applying
ApplyResourceChange
Executes the planned change:
Provider must:
Make actual infrastructure changes
Preserve all known values from planned_state
Replace unknown values with actual results
Return all-known state (no unknowns allowed)
Return error if actual result doesn’t match plan
Apply Implementation
State Evolution
func ( p * Provider ) ApplyResourceChange (
req ApplyResourceChangeRequest ,
) ApplyResourceChangeResponse {
var newState ResourceState
switch req . PlannedState . ID . IsNull () {
case true :
// Create new resource
resp := api . CreateInstance (
AMI : req . PlannedState . AMI ,
InstanceType : req . PlannedState . InstanceType ,
)
newState = ResourceState {
ID : resp . ID , // Was unknown
AMI : req . PlannedState . AMI , // Known in plan
InstanceType : req . PlannedState . InstanceType ,
PublicIP : resp . PublicIP , // Was unknown
}
case false :
// Update existing resource
api . UpdateInstance (
ID : req . PriorState . ID ,
AMI : req . PlannedState . AMI ,
)
newState = req . PlannedState // Already fully known
}
return ApplyResourceChangeResponse {
NewState : newState ,
}
}
Error handling:
If apply fails partway:
Return partial new state showing what was created
Include diagnostics explaining failure
Terraform saves partial state for recovery
See: docs/resource-instance-change-lifecycle.md:188
Special Cases
Import Workflow
Adopting existing infrastructure:
ImportResourceState responsibilities:
Accept provider-defined ID string
Query external system
Return minimal state for ReadResource to complete
May return partial state for write-only attributes
Partial import handling:
// Import stub (partial)
{
"id" : "i-abc123" ,
"password" : null // Write-only, cannot retrieve
}
// After ReadResource
{
"id" : "i-abc123" ,
"ami" : "ami-123456" ,
"instance_type" : "t2.micro" ,
"password" : null // User must provide in config
}
User must add password to configuration to match imported resource.
See: docs/resource-instance-change-lifecycle.md:318
Replace Triggered By
Forced replacement due to triggers:
resource "aws_instance" "example" {
ami = aws_ami . latest . id
lifecycle {
replace_triggered_by = [ aws_ami . latest ]
}
}
When aws_ami.latest changes:
Terraform marks aws_instance.example for replacement
PlanResourceChange receives indication of forced replacement
Provider plans destroy + create even if config unchanged
Create Before Destroy
Minimize downtime during replacement:
resource "aws_instance" "example" {
lifecycle {
create_before_destroy = true
}
}
Execution order:
Create new instance
Update dependencies to point to new instance
Destroy old instance
Requires careful handling of unique constraints (names, IPs, etc.).
Validation Rules
Terraform enforces contracts at each phase:
ValidateResourceConfig
// Called multiple times with increasing config completeness
validate ( config_with_unknowns ) // OK
validate ( config_mostly_known ) // OK
validate ( config_fully_known ) // Must pass
Should tolerate unknown values
Report errors for definitely-invalid configs
May return warnings for potentially-invalid configs
PlanResourceChange
Config Values
Computed Values
Plan Consistency
Rule: Non-null config values must be preserved
config.ami = "ami-123456"
planned_state.ami = "ami-123456" ✓ Correct
planned_state.ami = "ami-789012" ✗ Invalid
planned_state.ami = unknown ✗ Invalid
Rule: Optional+computed null in config → provider decides
config.public_ip = null
prior_state.public_ip = "52.1.2.3"
planned_state.public_ip = "52.1.2.3" ✓ Preserve
planned_state.public_ip = unknown ✓ Will change
planned_state.public_ip = "52.5.6.7" ✓ Provider knows
Rule: Second plan must not change known values
initial_plan.ami = "ami-123456"
final_plan.ami = "ami-123456" ✓ Identical
final_plan.ami = "ami-789012" ✗ Changed known value
initial_plan.public_ip = unknown
final_plan.public_ip = "52.1.2.3" ✓ Refined unknown
final_plan.public_ip = unknown ✓ Still unknown
See: docs/resource-instance-change-lifecycle.md:143
ApplyResourceChange
Rule: New state must match planned state for known values
planned_state.ami = "ami-123456"
new_state.ami = "ami-123456" ✓ Matches
new_state.ami = "ami-789012" ✗ Provider changed value!
planned_state.public_ip = unknown
new_state.public_ip = "52.1.2.3" ✓ Resolved unknown
new_state.public_ip = unknown ✗ Must be known now!
See: docs/resource-instance-change-lifecycle.md:202
Nested Blocks
Blocks have special rules:
resource "aws_instance" "example" {
ebs_block_device { # Block (not attribute)
device_name = "/dev/sda1"
}
}
Block constraints:
Number of blocks fixed during planning
Cannot add/remove blocks during apply
Each block must have corresponding planned block
Reporting extra blocks:
Use separate computed attribute:
resource "compute_instance" "example" {
network_interface { # User-specified
name = "eth0"
}
# Separate attribute for provider-created interfaces
all_network_interfaces = [ # Computed attribute
{ name = "eth0" }, # From block
{ name = "eth1" } # Auto-created by provider
]
}
See: docs/resource-instance-change-lifecycle.md:296
Further Reading
Plugin Protocol RPC method specifications
Graph Evaluation How resources are scheduled