Advanced State Management
State Evolution
As providers evolve, resource schemas change. Advanced state management ensures smooth migrations without data loss or user disruption.
State Schema Versions
Version Tracking
func (r *CacheResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// Current schema version
Version: 2,
Description: "Manages a cache instance",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
// v2: Renamed from instance_type
"node_type": schema.StringAttribute{
Required: true,
},
// v2: New attribute
"engine_version": schema.StringAttribute{
Optional: true,
Computed: true,
},
},
}
}
State Version History
Version 0 (initial):
├── id: string
├── name: string
└── instance_type: string
Version 1:
├── id: string
├── name: string
├── instance_type: string
└── engine: string (added)
Version 2 (current):
├── id: string
├── name: string
├── node_type: string (renamed from instance_type)
├── engine: string
└── engine_version: string (added)
State Upgraders
Implementing Upgraders
func (r *CacheResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
// Upgrade from version 0 to current
0: {
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
"instance_type": schema.StringAttribute{
Required: true,
},
},
},
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
// Define old state structure
type OldStateModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
InstanceType types.String `tfsdk:"instance_type"`
}
var oldState OldStateModel
resp.Diagnostics.Append(req.State.Get(ctx, &oldState)...)
if resp.Diagnostics.HasError() {
return
}
// Migrate to new structure
newState := CacheResourceModel{
ID: oldState.ID,
Name: oldState.Name,
NodeType: oldState.InstanceType, // Renamed field
Engine: types.StringValue("redis"), // Default value
}
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
},
},
// Upgrade from version 1 to current
1: {
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
"instance_type": schema.StringAttribute{
Required: true,
},
"engine": schema.StringAttribute{
Required: true,
},
},
},
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
type V1StateModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
InstanceType types.String `tfsdk:"instance_type"`
Engine types.String `tfsdk:"engine"`
}
var v1State V1StateModel
resp.Diagnostics.Append(req.State.Get(ctx, &v1State)...)
if resp.Diagnostics.HasError() {
return
}
// Migrate to v2
newState := CacheResourceModel{
ID: v1State.ID,
Name: v1State.Name,
NodeType: v1State.InstanceType, // Renamed
Engine: v1State.Engine,
// Fetch engine version from API
EngineVersion: types.StringUnknown(),
}
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
},
},
}
}
Testing State Upgrades
func TestCacheResource_StateUpgrade_V0ToV2(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
// Step 1: Create with old provider version
ExternalProviders: map[string]resource.ExternalProvider{
"cachecloud": {
VersionConstraint: "1.0.0", // Old version with v0 schema
Source: "example/cachecloud",
},
},
Config: `
resource "cachecloud_cache" "test" {
name = "test-cache"
instance_type = "cache.t3.micro"
}
`,
},
{
// Step 2: Upgrade to new provider version
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Config: `
resource "cachecloud_cache" "test" {
name = "test-cache"
node_type = "cache.t3.micro" # Renamed attribute
engine = "redis" # New default
}
`,
// Verify state upgraded successfully
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("cachecloud_cache.test", "name", "test-cache"),
resource.TestCheckResourceAttr("cachecloud_cache.test", "node_type", "cache.t3.micro"),
resource.TestCheckResourceAttr("cachecloud_cache.test", "engine", "redis"),
resource.TestCheckResourceAttrSet("cachecloud_cache.test", "engine_version"),
),
},
},
})
}
Complex State Migrations
Nested Attribute Changes
// Version 1: Flat structure
type V1NetworkConfig struct {
VPCID types.String `tfsdk:"vpc_id"`
SubnetID types.String `tfsdk:"subnet_id"`
}
// Version 2: Nested structure
type V2NetworkConfig struct {
VPC *VPCConfig `tfsdk:"vpc"`
Subnet *SubnetConfig `tfsdk:"subnet"`
}
type VPCConfig struct {
ID types.String `tfsdk:"id"`
CIDR types.String `tfsdk:"cidr"`
}
type SubnetConfig struct {
ID types.String `tfsdk:"id"`
AZ types.String `tfsdk:"availability_zone"`
}
// State upgrader
func upgradeV1ToV2(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var v1State struct {
NetworkConfig *V1NetworkConfig `tfsdk:"network_config"`
}
resp.Diagnostics.Append(req.State.Get(ctx, &v1State)...)
if resp.Diagnostics.HasError() {
return
}
// Fetch additional data from API
client := req.ProviderMeta.(*Client)
vpcDetails, err := client.GetVPC(ctx, v1State.NetworkConfig.VPCID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Migration Error",
fmt.Sprintf("Failed to fetch VPC details: %s", err))
return
}
subnetDetails, err := client.GetSubnet(ctx, v1State.NetworkConfig.SubnetID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Migration Error",
fmt.Sprintf("Failed to fetch subnet details: %s", err))
return
}
// Build new state
v2State := struct {
NetworkConfig *V2NetworkConfig `tfsdk:"network_config"`
}{
NetworkConfig: &V2NetworkConfig{
VPC: &VPCConfig{
ID: types.StringValue(vpcDetails.ID),
CIDR: types.StringValue(vpcDetails.CIDRBlock),
},
Subnet: &SubnetConfig{
ID: types.StringValue(subnetDetails.ID),
AZ: types.StringValue(subnetDetails.AvailabilityZone),
},
},
}
resp.Diagnostics.Append(resp.State.Set(ctx, v2State)...)
}
List to Set Conversion
// V1: List attribute
"security_groups": schema.ListAttribute{
ElementType: types.StringType,
}
// V2: Set attribute (order doesn't matter)
"security_groups": schema.SetAttribute{
ElementType: types.StringType,
}
// Upgrader
func upgradeListToSet(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var v1State struct {
SecurityGroups types.List `tfsdk:"security_groups"`
}
resp.Diagnostics.Append(req.State.Get(ctx, &v1State)...)
if resp.Diagnostics.HasError() {
return
}
// Extract list elements
var sgList []types.String
resp.Diagnostics.Append(v1State.SecurityGroups.ElementsAs(ctx, &sgList, false)...)
if resp.Diagnostics.HasError() {
return
}
// Convert to set
sgValues := make([]attr.Value, len(sgList))
for i, sg := range sgList {
sgValues[i] = sg
}
sgSet, diags := types.SetValue(types.StringType, sgValues)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
v2State := struct {
SecurityGroups types.Set `tfsdk:"security_groups"`
}{
SecurityGroups: sgSet,
}
resp.Diagnostics.Append(resp.State.Set(ctx, v2State)...)
}
Attribute Path Management
Reading Nested Attributes
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CacheResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Access nested attribute using path
var vpcID string
resp.Diagnostics.Append(req.State.GetAttribute(ctx,
path.Root("network_config").AtName("vpc").AtName("id"),
&vpcID)...)
tflog.Debug(ctx, "VPC ID from state", map[string]interface{}{
"vpc_id": vpcID,
})
}
Setting Nested Attributes
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// Update specific nested attribute
if !plan.NetworkConfig.VPC.CIDR.Equal(state.NetworkConfig.VPC.CIDR) {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("network_config").AtName("vpc").AtName("cidr"),
plan.NetworkConfig.VPC.CIDR)...)
}
}
Validating Attribute Paths
type networkConfigValidator struct{}
func (v networkConfigValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
// Get value at specific path
var vpcID string
diags := req.Config.GetAttribute(ctx,
req.Path.AtName("vpc").AtName("id"),
&vpcID)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
// Validate VPC ID format
if !isValidVPCID(vpcID) {
resp.Diagnostics.AddAttributeError(
req.Path.AtName("vpc").AtName("id"),
"Invalid VPC ID",
fmt.Sprintf("VPC ID '%s' is not in valid format", vpcID),
)
}
}
State Recovery
Handling Corrupted State
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CacheResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Attempt to fetch current state from API
cache, err := r.client.GetCache(ctx, state.ID.ValueString())
if err != nil {
if isNotFoundError(err) {
// Resource deleted outside Terraform
tflog.Warn(ctx, "Resource not found, removing from state",
map[string]interface{}{"id": state.ID.ValueString()})
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("API Error",
fmt.Sprintf("Failed to read cache: %s", err))
return
}
// Rebuild state from API response
newState := CacheResourceModel{
ID: types.StringValue(cache.ID),
Name: types.StringValue(cache.Name),
NodeType: types.StringValue(cache.NodeType),
}
// Preserve computed values if API doesn't return them
if cache.EngineVersion == "" {
newState.EngineVersion = state.EngineVersion
} else {
newState.EngineVersion = types.StringValue(cache.EngineVersion)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
}
Import with State Reconstruction
func (r *CacheResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import using ID
id := req.ID
// Fetch full resource details
cache, err := r.client.GetCache(ctx, id)
if err != nil {
resp.Diagnostics.AddError("Import Error",
fmt.Sprintf("Failed to import cache: %s", err))
return
}
// Reconstruct complete state
state := CacheResourceModel{
ID: types.StringValue(cache.ID),
Name: types.StringValue(cache.Name),
NodeType: types.StringValue(cache.NodeType),
Engine: types.StringValue(cache.Engine),
EngineVersion: types.StringValue(cache.EngineVersion),
}
// Fetch and populate nested attributes
if cache.NetworkConfig != nil {
state.NetworkConfig = &NetworkConfigModel{
VPCID: types.StringValue(cache.NetworkConfig.VPCID),
}
// Build subnet list
subnetValues := make([]attr.Value, len(cache.NetworkConfig.Subnets))
for i, subnet := range cache.NetworkConfig.Subnets {
subnetValues[i] = types.StringValue(subnet)
}
subnetList, diags := types.ListValue(types.StringType, subnetValues)
resp.Diagnostics.Append(diags...)
state.NetworkConfig.SubnetIDs = subnetList
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Handling Computed Attributes
Conditional Computation
schema.Int64Attribute{
Description: "Current connection count",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(), // Keep state value if plan unknown
},
}
Custom Plan Modifier
type conditionalComputeModifier struct{}
func (m conditionalComputeModifier) Description(ctx context.Context) string {
return "Conditionally recomputes based on configuration changes"
}
func (m conditionalComputeModifier) MarkdownDescription(ctx context.Context) string {
return "Conditionally recomputes based on configuration changes"
}
func (m conditionalComputeModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
// If configuration hasn't changed, use state value
var planConfig, stateConfig CacheResourceModel
req.Plan.Get(ctx, &planConfig)
req.State.Get(ctx, &stateConfig)
if planConfig.NodeType.Equal(stateConfig.NodeType) &&
planConfig.Engine.Equal(stateConfig.Engine) {
// No significant changes, keep state value
resp.PlanValue = req.StateValue
return
}
// Configuration changed, mark as unknown (will be computed)
resp.PlanValue = types.Int64Unknown()
}
Best Practices
1. Version Every Schema Change
// ✅ Good: Increment version
resp.Schema = schema.Schema{
Version: 3, // Incremented
}
// ❌ Bad: No version change
resp.Schema = schema.Schema{
Version: 2, // Not incremented despite changes
}
2. Provide Clear Migration Path
// ✅ Good: Handle each version
func (r *Resource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {/* v0 to v3 */},
1: {/* v1 to v3 */},
2: {/* v2 to v3 */},
}
}
// ❌ Bad: Skip versions
func (r *Resource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {/* v0 to v3, but v1 and v2 unsupported */},
}
}
3. Test State Upgrades
// ✅ Good: Test all upgrade paths
func TestStateUpgrade_V0ToV1(t *testing.T) {}
func TestStateUpgrade_V1ToV2(t *testing.T) {}
func TestStateUpgrade_V2ToV3(t *testing.T) {}
func TestStateUpgrade_V0ToV3(t *testing.T) {} // End-to-end
// ❌ Bad: No upgrade tests
4. Document Breaking Changes
# CHANGELOG.md
## [2.0.0] - 2026-07-01
### BREAKING CHANGES
- `instance_type` attribute renamed to `node_type`
- State will be automatically upgraded on first apply
- Backup your state before upgrading
### Migration
```hcl
# Old (v1.x)
resource "cachecloud_cache" "example" {
instance_type = "cache.t3.micro"
}
# New (v2.x)
resource "cachecloud_cache" "example" {
node_type = "cache.t3.micro" # Renamed
}
```
Key Takeaways
- ✅ Version schema for every change
- ✅ Implement state upgraders for migrations
- ✅ Test all upgrade paths thoroughly
- ✅ Use attribute paths for nested access
- ✅ Handle corrupted/missing state gracefully
- ✅ Provide clear migration documentation
- ✅ Preserve user data during upgrades
- ✅ Support importing existing resources
What’s Next?
Learn to build custom validators for complex business logic.
Previous: ← Performance Optimization
Next: Custom Validation →
Last updated: July 1, 2026