Migrate Terraform provider resources and data sources from Plugin SDKv2 to the Plugin Framework: muxing both plugins in one provider (terraform-plugin-mux, tf5to6server), per-resource migration workflow, SDKv2-to-Framework schema mapping (ForceNew, ValidateFunc, DiffSuppressFunc, Default, Timeouts, blocks), null-vs-zero-value behavioral traps, and state-compatibility verification. Use when converting or translating SDKv2 resources to the Framework, setting up a muxed provider server, deciding whether a resource should be migrated at all, or debugging plan diffs and state errors that appeared after a migration.
75
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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 pairsOfficial guide: Framework migration.
Migration has real risk and little user-visible payoff, so triage first:
DiffSuppressFunc,
no CustomizeDiff, no StateFunc, no complex nested blocks.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.
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:
"metadata": {"protocol_versions": ["6.0"]} in
terraform-registry-manifest.json.The migrated resource must be indistinguishable to users. Prove it with tests that exist before the migration:
_basic with 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.""/0/false is about
to matter), and any DiffSuppressFunc/StateFunc normalization.Translate schema and CRUD using the mapping table in
references/schema-mapping.md. The rules that prevent breaking changes:
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.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).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.StateUpgrader — treat that as a signal the
resource may be in the do-not-migrate bucket.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.
ImportStateVerify, which diffs imported state against
stored state and catches most null-vs-zero regressions.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 plan against a real pre-migration state file shows no diff.id keptUse 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.
3268468
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.