Module 8: State Management

5 minute read

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 JSON
  • mode: “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?

Start with State Basics →


Previous: ← Data Sources Practice

Last updated: July 1, 2026

Updated: