Module 8: State Management
Module Overview
State management is critical to Terraform provider development. Your provider must correctly track resource lifecycle, handle imports, detect configuration drift, and migrate state across schema versions.
What You’ll Learn:
- How Terraform state works
- Implementing resource import
- Detecting and handling drift
- Schema upgrades and state migrations
- Advanced state manipulation techniques
Time: 4-5 hours
Why State Management Matters
State is Terraform’s memory of your infrastructure:
┌─────────────────────────────────────────────────────┐
│ Terraform State │
├─────────────────────────────────────────────────────┤
│ │
│ Configuration ←──→ State ←──→ Real World │
│ (Desired) (Tracked) (Actual) │
│ │
│ resource "x" { { API: X exists │
│ name = "foo" "name": "foo" status: running│
│ size = "large" "size": "large"size: large │
│ } ... ... │
│ } │
└─────────────────────────────────────────────────────┘
During terraform plan:
1. Read state file
2. Refresh from API (detect drift)
3. Compare with configuration
4. Plan changes to reconcile
State Operations Your Provider Must Support
1. Create & Read
Store new resources in state after creation:
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Create via API
db, err := r.client.CreateDatabase(ctx, ...)
// Save to state
state := DatabaseResourceModel{
ID: types.StringValue(db.ID),
Name: types.StringValue(db.Name),
// ...
}
resp.State.Set(ctx, state) // ← Stored in state file
}
2. Update
Modify state when resources change:
func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Update via API
db, err := r.client.UpdateDatabase(ctx, ...)
// Update state
state.Name = types.StringValue(db.Name)
resp.State.Set(ctx, state) // ← Updated in state
}
3. Delete
Remove from state when resources are destroyed:
func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Delete via API
r.client.DeleteDatabase(ctx, state.ID.ValueString())
// State automatically removed by framework
}
4. Import
Bring existing resources under management:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// User runs: terraform import mycloud_database.db db-12345
id := req.ID // "db-12345"
// Fetch from API
db, err := r.client.GetDatabase(ctx, id)
// Populate state
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
5. Refresh
Update state from API (detect drift):
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
// Fetch current state from API
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx) // ← Deleted outside Terraform
return
}
// Update state with current values
state.Name = types.StringValue(db.Name)
state.Status = types.StringValue(db.Status)
resp.State.Set(ctx, state) // ← State refreshed
}
State File Structure
Terraform stores state in JSON format:
{
"version": 4,
"terraform_version": "1.6.0",
"resources": [
{
"mode": "managed",
"type": "mycloud_database",
"name": "primary",
"provider": "provider[\"registry.terraform.io/yourorg/mycloud\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "db-12345",
"name": "production-db",
"engine": "postgres",
"engine_version": "14.5",
"storage_gb": 100,
"tags": {
"Environment": "production"
}
}
}
]
}
]
}
Key Fields:
schema_version: Your resource’s schema version (for migrations)attributes: All resource attributes stored as JSONmode: “managed” for resources, “data” for data sources
Common State Scenarios
Scenario 1: Import Existing Resources
User has databases created outside Terraform:
# Before: Resource exists but not tracked
$ mycloud-cli list-databases
db-12345 production-db postgres running
# Import into Terraform
$ terraform import mycloud_database.db db-12345
# After: Resource tracked in state
$ terraform state list
mycloud_database.db
Your provider must implement ImportState.
Scenario 2: Detect Drift
Resource modified outside Terraform:
# Someone changed storage via console
$ terraform plan
mycloud_database.db: Refreshing state...
Terraform will perform the following actions:
# mycloud_database.db will be updated in-place
~ resource "mycloud_database" "db" {
id = "db-12345"
~ storage_gb = 200 -> 100 # Drift detected!
}
Your provider’s Read operation must detect changes.
Scenario 3: Schema Migration
You release a new provider version with schema changes:
// Version 1.0.0
"storage": schema.Int64Attribute{...}
// Version 2.0.0 - Changed to string with unit
"storage": schema.StringAttribute{...} // "100GB"
Your provider must implement UpgradeState to migrate old state.
Scenario 4: Resource Deleted Outside Terraform
# Someone deleted database via API
$ terraform plan
mycloud_database.db: Refreshing state...
Terraform detected the following changes made outside of Terraform since the last "terraform apply":
# mycloud_database.db has been deleted
- resource "mycloud_database" "db" {
- id = "db-12345" -> null
}
This is a destroy change, requiring replacement.
Your Read operation must call resp.State.RemoveResource(ctx) when not found.
Module Chapters
Chapter 1: State Basics
Understanding state structure, schema versions, and how state is stored.
Chapter 2: Resource Import
Implementing ImportState to bring existing resources under management.
Chapter 3: State Refresh
Reading current state from APIs and updating Terraform’s view.
Chapter 4: Drift Detection
Detecting and handling changes made outside Terraform.
Chapter 5: Schema Upgrades
Migrating state when schema changes between provider versions.
Chapter 6: State Manipulation
Advanced state operations, remote state, and state locking.
Chapter 7: State Management Lab
Hands-on: Implement import, handle drift, and migrate schema versions.
Chapter 8: Practice Questions
Test your understanding with comprehensive practice scenarios.
Learning Path
State Basics → Import → Refresh → Drift → Upgrades → Advanced
↓ ↓ ↓ ↓ ↓ ↓
Concepts Existing Read() Detect Migrations Remote
Resources Changes State
What You’ll Build
By the end of this module, you’ll have implemented:
Import Support:
terraform import mycloud_database.db db-12345
Drift Detection:
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Detect changes made outside Terraform
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
// Update state with actual values
state.StorageGB = types.Int64Value(int64(db.StorageGB))
}
Schema Migration:
func (r *DatabaseResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {
PriorSchema: &schema.Schema{ /* v0 schema */ },
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
// Migrate v0 state to v1
},
},
}
}
Prerequisites
Before starting this module, you should have completed:
- ✅ Module 6: Resource Implementation
- ✅ Module 7: Data Sources
Key Concepts
State: Terraform’s record of managed infrastructure Import: Adding existing resources to state Refresh: Updating state from API to match reality Drift: Differences between state and actual infrastructure Schema Version: Integer tracking schema evolution State Upgrade: Migrating state when schema changes
Best Practices Preview
Throughout this module, you’ll learn:
- ✅ Always implement import for resources
- ✅ Detect drift in Read operations
- ✅ Handle deleted resources gracefully
- ✅ Version schemas for backward compatibility
- ✅ Test import and migration paths
- ✅ Log state operations for debugging
- ✅ Never modify state directly (use framework)
- ✅ Document import ID format clearly
Common Pitfalls to Avoid
- ❌ Forgetting to implement ImportState
- ❌ Not detecting drift in Read
- ❌ Returning error when resource not found (should remove from state)
- ❌ Breaking schema changes without migration
- ❌ Not testing import functionality
- ❌ Assuming state always matches reality
Real-World Impact
Proper state management enables:
1. Brownfield Adoption: Import existing infrastructure into Terraform
# Import 100 existing databases
for db in $(mycloud-cli list-database-ids); do
terraform import "mycloud_database.db_${db}" "${db}"
done
2. Drift Detection: Identify manual changes
terraform plan # Shows drift before applying changes
3. Safe Upgrades: Migrate to new provider versions seamlessly
# Upgrade provider
terraform init -upgrade
terraform plan # State automatically migrated
4. Disaster Recovery: Rebuild from state
# Lost infrastructure but have state backup
terraform apply # Recreates everything
Get Started
Ready to master state management?
Previous: ← Data Sources Practice
Last updated: July 1, 2026