State Migration and Schema Upgrades
Relevant source files
The following files were used as context for generating this wiki page:
- internal/service/ec2/instance_migrate.go
- internal/service/ecs/task_definition_migrate.go
- internal/service/dynamodb/table_migrate.go
- internal/service/firehose/delivery_stream_migrate.go
- providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tfplugin6/tfplugin6.proto
- providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5/tfplugin5.proto
- internal/service/ssm/document_test.go
State migration is a mechanism within the Terraform provider that allows for the evolution of resource schemas over time. When a resource's schema changes between provider versions (e.g., an attribute is renamed, removed, or restructured), this mechanism automatically upgrades a user's existing Terraform state to conform to the new schema. This process is crucial for maintaining backward compatibility and providing a smooth upgrade path for users, preventing breaking changes that would otherwise require manual state manipulation.
The process is triggered by Terraform Core when it detects that the schema version of a resource stored in the state file is lower than the current schema version defined in the provider. The provider then executes a series of migration functions to bring the state up to the latest version, one version at a time. This is handled via the UpgradeResourceState RPC call defined in the Terraform plugin protocol.
Core Concepts: State and Schema Versioning
At the heart of the migration system is the concept of schema versioning. Each resource schema can be assigned a version number. This version is an integer that is incremented whenever a backward-incompatible change is made to the resource's schema.
- Schema Version: A version number is defined within the resource's
Schemastruct. This version is persisted in the Terraform state file for each resource instance. - State Upgraders: For each version increment, a corresponding migration function is implemented. These functions are registered in the
StateUpgradersfield of theschema.Resource.
When Terraform plans an action for a resource, it compares the schema_version from the state file with the one in the provider's code. If the state's version is older, Terraform invokes the state upgrade process before proceeding with the plan.
Sources: providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tfplugin6/tfplugin6.proto:115-119, internal/service/dynamodb/table_migrate.go:12-16
The UpgradeResourceState RPC Call
The UpgradeResourceState RPC is the formal interface through which Terraform Core requests a provider to upgrade a resource's state. This is defined in the plugin protocol.
The diagram below illustrates the sequence of events when a state upgrade is required.
This diagram shows the interaction between Terraform Core and the provider plugin during a state upgrade.
Sources: providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tfplugin6/tfplugin6.proto:130-132, providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5/tfplugin5.proto:141-143
RPC Message Structure
The request and response for the UpgradeResourceState call have a well-defined structure.
UpgradeResourceState.Request
| Field | Type | Description |
|---|---|---|
type_name | string | The name of the resource type that needs upgrading (e.g., aws_dynamodb_table). |
version | int64 | The schema version of the state as recorded in the state file. |
raw_state | RawState | The raw state data, which can be either JSON or a legacy flatmap format. |
UpgradeResourceState.Response
| Field | Type | Description |
|---|---|---|
upgraded_state | DynamicValue | The new state data, encoded in msgpack, which conforms to the target schema version. |
diagnostics | repeated Diagnostic | A list of errors or warnings encountered during the migration. |
Sources: providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tfplugin6/tfplugin6.proto:186-204, providerlint/vendor/github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5/tfplugin5.proto:159-177
State Migration Implementation
In the provider, migration logic is implemented via a series of functions, each responsible for upgrading from one specific version to the next. These are attached to the resource definition.
A top-level migration function, often named resource<ResourceName>MigrateState or simply MigrateState, acts as a dispatcher. It receives the schema version from the state and calls the appropriate version-specific migration function.
The general flow of a migration function is as follows:
This flowchart depicts the common steps within a state migration function.
Sources: internal/service/dynamodb/table_migrate.go:12-16, internal/service/firehose/delivery_stream_migrate.go:11-15
Migration Examples
The provider contains numerous examples of state migrations. Below are two representative cases.
DynamoDB Table Migration (v0 to v1)
Sources: internal/service/dynamodb/table_migrate.go
In version 0 of the aws_dynamodb_table resource, the global_secondary_index attribute was stored in a flat structure. The v1 schema likely introduced a more structured block. The migration function migrateDynamoDBStateV0toV1 handles this transition.
Logic:
- It reads the old
global_secondary_indexattributes using aschema.MapFieldReader. - It deletes all the old keys that have the
global_secondary_index.prefix. - It uses a
schema.MapFieldWriterto write the data back into the state attributes, conforming to the new, nested structure.
State Transformation Example:
| State Before (v0) | State After (v1) |
|---|---|
global_secondary_index.2138343934.name | global_secondary_index.# = 1 |
global_secondary_index.2138343934.projection_type | global_secondary_index.0.name |
... | global_secondary_index.0.projection_type |
... |
Sources: internal/service/dynamodb/table_migrate.go:23-53
Kinesis Firehose Delivery Stream Migration (v0 to v1)
Sources: internal/service/firehose/delivery_stream_migrate.go
In version 0 of the aws_kinesis_firehose_delivery_stream resource, S3 configuration attributes like role_arn and s3_bucket_arn were top-level attributes. The v1 schema consolidated these into a new s3_configuration block.
Logic:
The migrateKinesisFirehoseV0toV1 function performs the following steps:
- It creates the new
s3_configurationblock by settings3_configuration.#to"1". - It copies the values from the old top-level attributes (e.g.,
role_arn) to the new nested attributes (e.g.,s3_configuration.0.role_arn). - It deletes the now-obsolete top-level attributes.
// internal/service/firehose/delivery_stream_migrate.go:28-30
is.Attributes["s3_configuration.#"] = "1"
is.Attributes["s3_configuration.0.role_arn"] = is.Attributes["role_arn"]
is.Attributes["s3_configuration.0.bucket_arn"] = is.Attributes["s3_bucket_arn"]
// internal/service/firehose/delivery_stream_migrate.go:43-48
delete(is.Attributes, "role_arn")
delete(is.Attributes, "s3_bucket_arn")
delete(is.Attributes, "s3_buffer_size")
delete(is.Attributes, "s3_data_compression")
delete(is.Attributes, "s3_buffer_interval")
delete(is.Attributes, "s3_prefix")
Sources: internal/service/firehose/delivery_stream_migrate.go:17-49
Testing Migrations and Updates
To ensure migrations and updates work as expected, acceptance tests are written to cover schema changes. For example, tests for the aws_ssm_document resource verify that updating the content correctly increments the latest_version and default_version attributes, simulating a resource evolution that state migration is designed to handle gracefully.
Sources: internal/service/ssm/document_test.go:163-186
Summary
State Migration and Schema Upgrades are a fundamental feature of the AWS provider, enabling non-disruptive evolution of resource schemas. By versioning schemas and implementing targeted migration functions, the provider can automatically update a user's state file to match newer, more robust schema definitions. This process, orchestrated by Terraform Core via the UpgradeResourceState RPC, is essential for maintaining a stable and user-friendly experience across different versions of the provider.