見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。
Migrating from Plugin SDKv2 to the Plugin Framework
The Plugin Framework is required for net-new resources and data sources; SDKv2 is maintenance-only. Migration is per-resource and incremental: a muxed provider serves SDKv2 and Framework implementations side by side, so you never need a big-bang rewrite. This skill covers the mux setup, the per-resource workflow, and the behavioral traps that turn a mechanical translation into a silent breaking change.
Reference (load when needed):
references/schema-mapping.md— the full SDKv2 → Framework translation
table with code pairs
Official guide: Framework migration.
Decide Whether to Migrate at All
Migration has real risk and little user-visible payoff, so triage first:
- Do not migrate complex or heavily-used resources without a driving
need (a Framework-only feature, a bug that SDKv2 cannot fix). The two SDKs differ behaviorally — most importantly around null versus zero values — and those differences surface as breaking changes for existing users. This is the standing policy in large providers like terraform-provider-aws.
- Simple resources migrate safely: flat schemas, no
DiffSuppressFunc,
no CustomizeDiff, no StateFunc, no complex nested blocks.
- New capabilities never require migrating old code — mux and write the new
resource in the Framework alongside the old ones.
To tell what mode a provider is in, check go.mod: terraform-plugin-mux present means it already serves both; only terraform-plugin-sdk/v2 means SDKv2-only (mux setup is your first step); only terraform-plugin-framework means the migration is done.
Step 1: Mux the Provider
Combine both plugin servers in main.go. Serving protocol version 6 requires upgrading the SDKv2 server with tf5to6server (protocol 6 needs Terraform CLI >= 1.0; if you must support 0.12+, mux at protocol 5 with tf6to5server/tf5muxserver instead — but the Framework provider then cannot use protocol-6-only features like nested attributes):
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server"
"github.com/hashicorp/terraform-plugin-mux/tf5to6server"
"github.com/hashicorp/terraform-plugin-mux/tf6muxserver"
"example.org/terraform-provider-examplecloud/internal/provider"
sdkprovider "example.org/terraform-provider-examplecloud/internal/sdkprovider"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "run with support for debuggers")
flag.Parse()
ctx := context.Background()
upgradedSDKServer, err := tf5to6server.UpgradeServer(
ctx,
sdkprovider.Provider().GRPCProvider,
)
if err != nil {
log.Fatal(err)
}
providers := []func() tfprotov6.ProviderServer{
providerserver.NewProtocol6(provider.New(version)()),
func() tfprotov6.ProviderServer { return upgradedSDKServer },
}
muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...)
if err != nil {
log.Fatal(err)
}
var serveOpts []tf6server.ServeOpt
if debug {
serveOpts = append(serveOpts, tf6server.WithManagedDebug())
}
err = tf6server.Serve("registry.terraform.io/example/examplecloud",
muxServer.ProviderServer, serveOpts...)
if err != nil {
log.Fatal(err)
}
}Mux requirements that bite in practice:
- Provider schemas must match exactly across both plugins — same
provider-level attributes, same types, same descriptions. Keep one source of truth for the provider configuration and mirror it.
- Each resource and data source may exist in only one of the two
plugins. Migration's final step is deleting the SDKv2 registration.
- If publishing to the Registry with protocol 6, set
"metadata": {"protocol_versions": ["6.0"]} in terraform-registry-manifest.json.
Step 2: Baseline Before You Touch Anything
The migrated resource must be indistinguishable to users. Prove it with tests that exist before the migration:
- Ensure the resource has passing acceptance coverage:
_basicwith an
import step (ImportStateVerify: true), _disappears, and per-attribute update tests. If coverage is missing, write it against the SDKv2 implementation first — these tests are the migration's acceptance criteria and must pass unchanged afterward.
- Note behaviors tests don't capture: attribute defaults, what happens
when optional attributes are omitted (null vs ""/0/false is about to matter), and any DiffSuppressFunc/StateFunc normalization.
Step 3: Port the Resource
Translate schema and CRUD using the mapping table in references/schema-mapping.md. The rules that prevent breaking changes:
- Blocks stay blocks. An SDKv2
Elem: &schema.Resource{...}written as
block { ... } syntax in user configs must become a Framework Block (schema.ListNestedBlock/SetNestedBlock) — converting it to a nested attribute changes the HCL syntax users must write, which is a breaking change. Nested attributes are for new schema only.
- Null is not zero. SDKv2
d.Get("name")returned""for unset;
the Framework model gives you types.String that distinguishes null, unknown, and "". Everywhere the old code checked == "" or relied on GetOk, decide explicitly what null means, and make sure you send the API the same thing SDKv2 sent (usually: omit the field when null).
- Keep the `id` attribute. Net-new Framework resources may omit a
redundant id, but a migrated resource must keep its exact schema — removing or renaming attributes breaks existing state and configs.
- State must round-trip. The Framework reads the state SDKv2 wrote. If
every attribute keeps its name and type, no state upgrade is needed. If the old schema stored a value the new types package normalizes differently, you need a StateUpgrader — treat that as a signal the resource may be in the do-not-migrate bucket.
Step 4: Move the Registration
Register the resource in the Framework provider's Resources() and delete it from the SDKv2 provider's ResourcesMap in the same commit — mux errors on duplicates.
Step 5: Verify
- The pre-existing acceptance tests pass without modification —
especially ImportStateVerify, which diffs imported state against stored state and catches most null-vs-zero regressions.
- Add a state-compatibility step: apply a config with the last released
(SDKv2) provider version, then plan with the migrated build — the plan must be empty. In terraform-plugin-testing this is a two-step test using ExternalProviders for the old version, then ProtoV6ProviderFactories with ConfigPlanChecks asserting an empty plan. The provider-test-patterns skill (if available) documents the pattern.
terraform planagainst a real pre-migration state file shows no diff.
Checklist
- [ ] Resource is simple enough to migrate (no complex diff customization), or there's a driving need
- [ ] Mux serves both plugins; provider-level schemas identical in both
- [ ] Acceptance tests existed before migration and pass unchanged after
- [ ] Blocks remained blocks; attribute names and types unchanged;
idkept - [ ] Null/omitted semantics preserved (API receives what SDKv2 sent)
- [ ] SDKv2 registration removed in the same change
- [ ] Empty-plan verified against state written by the previous release
- [ ] Changelog entry added, if the repo tracks release notes
Related Skills
Use the provider-resources skill (if available) for Framework CRUD, finder, and waiter patterns in the ported code, and provider-test-patterns for the regression and version-upgrade test patterns.

