• HashiCorp Developer

  • HashiCorp Cloud Platform
  • Terraform
  • Packer
  • Consul
  • Vault
  • Boundary
  • Nomad
  • Waypoint
  • Vagrant
Terraform
  • Install
  • Tutorials
    • About the Docs
    • Configuration Language
    • Terraform CLI
    • Terraform Cloud
    • Terraform Enterprise
    • CDK for Terraform
    • Provider Use
    • Plugin Development
    • Registry Publishing
    • Integration Program
  • Registry(opens in new tab)
  • Try Cloud(opens in new tab)
  • Sign up
Plugin Development

Framework

Skip to main content
  • Framework
  • Provider Servers
    • Overview
    • Create
    • Read
    • Update
    • Delete
    • Configure Clients
    • Validate Configuration
    • Modify Plan
    • Import State
    • Upgrade State
    • Manage Private State
    • Timeouts
  • Returning Errors and Warnings
  • Validation
  • Acceptance Tests
  • Debugging

  • Resources

  • Tutorial Library
  • Certifications
  • Community Forum
    (opens in new tab)
  • Support
    (opens in new tab)
  • GitHub
    (opens in new tab)
  • Terraform Registry
    (opens in new tab)
  1. Developer
  2. Terraform
  3. Plugin Development
  4. Framework
  5. Resources
  6. Upgrade State
  • Plugin Framework
  • v1.0.x
  • v0.17.x
  • v0.16.x
  • v0.15.x
  • v0.14.x
  • v0.13.x
  • v0.12.x
  • v0.11.x
  • v0.10.x
  • v0.9.x
  • v0.8.x
  • v0.7.x

»State Upgrade

A resource schema captures the structure and types of the resource state. Any state data that does not conform to the resource schema will generate errors or may not be persisted properly. Over time, it may be necessary for resources to make breaking changes to their schemas, such as changing an attribute type. Terraform supports versioning of these resource schemas and the current version is saved into the Terraform state. When the provider advertises a newer schema version, Terraform will call back to the provider to attempt to upgrade from the saved schema version to the one advertised. This operation is performed prior to planning, but with a configured provider.

Some versions of Terraform CLI will also request state upgrades even when the current schema version matches the state version. The framework will automatically handle this request.

State Upgrade Process

  1. When generating a plan, Terraform CLI will request the current resource schema, which contains a version.
  2. If Terraform CLI detects that an existing state with its saved version does not match the current version, Terraform CLI will request a state upgrade from the provider with the prior state version and expecting the state to match the current version.
  3. The framework will check the resource to see if it defines state upgrade support:
    • If no state upgrade support is defined, an error diagnostic is returned.
    • If state upgrade support is defined, but not for the requested prior state version, an error diagnostic is returned.
    • If state upgrade support is defined and has an implementation for the requested prior state version, the provider defined implementation is executed.

Implementing State Upgrade Support

Ensure the schema.Schema type Version field for the resource.Resource is greater than 0, then implement the resource.ResourceWithStateUpgrade interface for the resource.Resource. Conventionally the version is incremented by 1 for each state upgrade.

This example shows a Resource with the necessary StateUpgrade method to implement the ResourceWithStateUpgrade interface:

// Other Resource methods are omitted in this example
var _ resource.Resource = &ThingResource{}
var _ resource.ResourceWithUpgradeState = &ThingResource{}

type ThingResource struct{/* ... */}

func (r *ThingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        // ... other fields ...

        // This example conventionally declares that the resource has prior
        // state versions of 0 and 1, while the current version is 2.
        Version: 2,
    }
}

func (r *ThingResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
    return map[int64]resource.StateUpgrader{
        // State upgrade implementation from 0 (prior state version) to 2 (Schema.Version)
        0: {
            // Optionally, the PriorSchema field can be defined.
            StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { /* ... */ },
        },
        // State upgrade implementation from 1 (prior state version) to 2 (Schema.Version)
        1: {
            // Optionally, the PriorSchema field can be defined.
            StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { /* ... */ },
        },
    }
}

Each resource.StateUpgrader implementation is expected to wholly upgrade the resource state from the prior version to the current version. The framework does not iterate through intermediate version implementations as incrementing versions by 1 is only conventional and not required.

All state data must be populated in the resource.UpgradeStateResponse. The framework does not copy any prior state data from the resource.UpgradeStateRequest.

There are two approaches to implementing the provider logic for state upgrades in StateUpgrader. The recommended approach is defining the prior schema matching the resource state, which allows for prior state access similar to other parts of the framework. The second, more advanced, approach is accessing the prior resource state using lower level data handlers.

StateUpgrader With PriorSchema

Implement the StateUpgrader type PriorSchema field to enable the framework to populate the resource.UpgradeStateRequest type State field for the provider defined state upgrade logic. Access the request State using methods such as Get() or GetAttribute(). Write the resource.UpgradeStateResponse type State field using methods such as Set() or SetAttribute().

This example shows a resource that changes the type for two attributes, using the PriorSchema approach:

// Other Resource methods are omitted in this example
var _ resource.Resource = &ThingResource{}
var _ resource.ResourceWithUpgradeState = &ThingResource{}

type ThingResource struct{/* ... */}

type ThingResourceModelV0 struct {
    Id                string `tfsdk:"id"`
    OptionalAttribute *bool  `tfsdk:"optional_attribute"`
    RequiredAttribute bool   `tfsdk:"required_attribute"`
}

type ThingResourceModelV1 struct {
    Id                string  `tfsdk:"id"`
    OptionalAttribute *string `tfsdk:"optional_attribute"`
    RequiredAttribute string  `tfsdk:"required_attribute"`
}

func (r *ThingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Computed: true,
            },
            "optional_attribute": schema.StringAttribute{
                // As compared to prior schema.BoolAttribute below
                Optional: true,
            },
            "required_attribute": schema.StringAttribute{
                // As compared to prior schema.BoolAttribute below
                Required: true,
            },
        },
        // The resource has a prior state version of 0, which had the attribute
        // types of types.BoolType as shown below.
        Version: 1,
    }
}

func (r *ThingResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
    return map[int64]resource.StateUpgrader{
        // State upgrade implementation from 0 (prior state version) to 1 (Schema.Version)
        0: {
            PriorSchema: &schema.Schema{
                Attributes: map[string]schema.Attribute{
                    "id": schema.StringAttribute{
                        Computed: true,
                    },
                    "optional_attribute": schema.BoolAttribute{
                        // As compared to current schema.StringAttribute above
                        Optional: true,
                    },
                    "required_attribute": schema.BoolAttribute{
                        // As compared to current schema.StringAttribute above
                        Required: true,
                    },
                },
            },
            StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
                var priorStateData ThingResourceModelV0

                resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)

                if resp.Diagnostics.HasError() {
                    return
                }

                upgradedStateData := ThingResourceModelV1{
                    Id:                priorStateData.Id,
                    RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute),
                }

                if priorStateData.OptionalAttribute != nil {
                    v := fmt.Sprintf("%t", *priorStateData.OptionalAttribute)
                    upgradedStateData.OptionalAttribute = &v
                }

                resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
            },
        },
    }
}

StateUpgrader Without PriorSchema

Read prior state data from the resource.UpgradeStateRequest type RawState field. Write the resource.UpgradeStateResponse type State field using methods such as Set() or SetAttribute(), or for more advanced use cases, write the resource.UpgradeStateResponse type DynamicValue field.

This example shows a resource that changes the type for two attributes, using the RawState approach for the request and DynamicValue approach for the response:

// Other Resource methods are omitted in this example
var _ resource.Resource = &ThingResource{}
var _ resource.ResourceWithUpgradeState = &ThingResource{}

var ThingResourceTftypesDataV0 = tftypes.Object{
    AttributeTypes: map[string]tftypes.Type{
        "id":                 tftypes.String,
        "optional_attribute": tftypes.Bool,
        "required_attribute": tftypes.Bool,
    },
}

var ThingResourceTftypesDataV1 = tftypes.Object{
    AttributeTypes: map[string]tftypes.Type{
        "id":                 tftypes.String,
        "optional_attribute": tftypes.String,
        "required_attribute": tftypes.String,
    },
}

type ThingResource struct{/* ... */}

func (r *ThingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Computed: true,
            },
            "optional_attribute": schema.StringAttribute{
                // As compared to prior schema.BoolAttribute below
                Optional: true,
            },
            "required_attribute": schema.StringAttribute{
                // As compared to prior schema.BoolAttribute below
                Required: true,
            },
        },
        // The resource has a prior state version of 0, which had the attribute
        // types of types.BoolType as shown below.
        Version: 1,
    }
}

func (r *ThingResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
    return map[int64]resource.StateUpgrader{
        // State upgrade implementation from 0 (prior state version) to 1 (Schema.Version)
        0: {
            StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
                // Refer also to the RawState type JSON field which can be used
                // with json.Unmarshal()
                rawStateValue, err := req.RawState.Unmarshal(ThingResourceTftypesDataV0)

                if err != nil {
                    resp.Diagnostics.AddError(
                        "Unable to Unmarshal Prior State",
                        err.Error(),
                    )
                    return
                }

                var rawState map[string]tftypes.Value

                if err := rawStateValue.As(&rawState); err != nil {
                    resp.Diagnostics.AddError(
                        "Unable to Convert Prior State",
                        err.Error(),
                    )
                    return
                }

                var optionalAttributeString *string

                if !rawState["optional_attribute"].IsNull() {
                    var optionalAttribute bool

                    if err := rawState["optional_attribute"].As(&optionalAttribute); err != nil {
                        resp.Diagnostics.AddAttributeError(
                            path.Root("optional_attribute"),
                            "Unable to Convert Prior State",
                            err.Error(),
                        )
                        return
                    }

                    v := fmt.Sprintf("%t", optionalAttribute)
                    optionalAttributeString = &v
                }

                var requiredAttribute bool

                if err := rawState["required_attribute"].As(&requiredAttribute); err != nil {
                    resp.Diagnostics.AddAttributeError(
                        path.Root("required_attribute"),
                        "Unable to Convert Prior State",
                        err.Error(),
                    )
                    return
                }

                dynamicValue, err := tfprotov6.NewDynamicValue(
                    ThingResourceTftypesDataV1,
                    tftypes.NewValue(ThingResourceTftypesDataV1, map[string]tftypes.Value{
                        "id":                 rawState["id"],
                        "optional_attribute": tftypes.NewValue(tftypes.String, optionalAttributeString),
                        "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", requiredAttribute)),
                    }),
                )

                if err != nil {
                    resp.Diagnostics.AddError(
                        "Unable to Convert Upgraded State",
                        err.Error(),
                    )
                    return
                }

                resp.DynamicValue = &dynamicValue
            },
        },
    }
}
Edit this page on GitHub

On this page

  1. State Upgrade
  2. State Upgrade Process
  3. Implementing State Upgrade Support
Give Feedback(opens in new tab)
  • Certifications
  • System Status
  • Terms of Use
  • Security
  • Privacy
  • Trademark Policy
  • Trade Controls
  • Give Feedback(opens in new tab)