provider-framework-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrating from Plugin SDKv2 to the Plugin Framework
从Plugin SDKv2迁移到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):
- — the full SDKv2 → Framework translation table with code pairs
references/schema-mapping.md
Official guide: Framework migration.
Plugin Framework是新增资源和数据源的必备工具;SDKv2仅用于维护。迁移采用逐资源、增量式方式:混合Provider可同时运行SDKv2和Framework实现,因此无需一次性重写全部代码。本技能涵盖混合服务器搭建、逐资源迁移流程,以及可能导致隐性破坏性变更的行为陷阱。
参考资料(按需查阅):
- — 完整的SDKv2 → Framework转换对照表,包含代码示例对
references/schema-mapping.md
官方指南:Framework迁移
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 , no
DiffSuppressFunc, noCustomizeDiff, no complex nested blocks.StateFunc - 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 :
present means it already serves both; only means
SDKv2-only (mux setup is your first step); only
means the migration is done.
go.modterraform-plugin-muxterraform-plugin-sdk/v2terraform-plugin-framework迁移存在实际风险,且用户可见收益有限,因此需先进行优先级划分:
- 若无明确需求(如仅Framework支持的功能、SDKv2无法修复的Bug),请勿迁移复杂或高频使用的资源。两个SDK在行为上存在差异——最关键的是空值与零值的处理差异——这些差异会对现有用户造成破坏性变更。这是terraform-provider-aws等大型Provider的既定策略。
- 简单资源可安全迁移:扁平化Schema、无、无
DiffSuppressFunc、无CustomizeDiff、无复杂嵌套块。StateFunc - 新增功能无需迁移旧代码——可在Framework中编写新资源,与旧资源通过混合方式共存。
要判断Provider所处模式,可查看:若存在,说明已同时运行两种插件;仅存在则为纯SDKv2模式(第一步需搭建混合服务器);仅存在则表示迁移已完成。
go.modterraform-plugin-muxterraform-plugin-sdk/v2terraform-plugin-frameworkStep 1: Mux the Provider
步骤1:搭建混合Provider
Combine both plugin servers in . Serving protocol version 6
requires upgrading the SDKv2 server with (protocol 6 needs
Terraform CLI >= 1.0; if you must support 0.12+, mux at protocol 5 with
/ instead — but the Framework provider then
cannot use protocol-6-only features like nested attributes):
main.gotf5to6servertf6to5servertf5muxservergo
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
in
"metadata": {"protocol_versions": ["6.0"]}.terraform-registry-manifest.json
在中合并两个插件服务器。要支持协议版本6,需使用升级SDKv2服务器(协议6要求Terraform CLI >= 1.0;若必须支持0.12+,则需使用/在协议5下混合——但此时Framework Provider无法使用仅协议6支持的功能,如嵌套属性):
main.gotf5to6servertf6to5servertf5muxservergo
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)
}
}实际使用中需注意的混合要求:
- 两个插件的Provider Schema必须完全一致——相同的Provider级属性、相同类型、相同描述。需保持Provider配置的单一数据源并同步。
- 每个资源和数据源只能存在于其中一个插件中。迁移的最后一步是删除SDKv2中的注册信息。
- 若要以协议6发布至Registry,需在中设置
terraform-registry-manifest.json。"metadata": {"protocol_versions": ["6.0"]}
Step 2: Baseline Before You Touch Anything
步骤2:迁移前建立基准
The migrated resource must be indistinguishable to users. Prove it with
tests that exist before the migration:
- Ensure the resource has passing acceptance coverage: with an import step (
_basic),ImportStateVerify: true, 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._disappears - Note behaviors tests don't capture: attribute defaults, what happens
when optional attributes are omitted (null vs /
""/0is about to matter), and anyfalse/DiffSuppressFuncnormalization.StateFunc
迁移后的资源对用户而言必须无差异。在迁移前通过测试验证这一点:
- 确保资源有通过的验收测试覆盖:包含导入步骤的测试(
_basic)、ImportStateVerify: true测试,以及逐属性更新测试。若测试覆盖不足,需先针对SDKv2实现编写测试——这些测试是迁移的验收标准,迁移后必须无修改通过。_disappears - 记录测试未覆盖的行为:属性默认值、省略可选属性时的表现(空值与/
""/0的区别即将变得重要),以及任何false/DiffSuppressFunc的标准化处理。StateFunc
Step 3: Port the Resource
步骤3:迁移资源
Translate schema and CRUD using the mapping table in
. The rules that prevent breaking changes:
references/schema-mapping.md- Blocks stay blocks. An SDKv2 written as
Elem: &schema.Resource{...}syntax in user configs must become a Framework Block (block { ... }/schema.ListNestedBlock) — 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.SetNestedBlock - Null is not zero. SDKv2 returned
d.Get("name")for unset; the Framework model gives you""that distinguishes null, unknown, andtypes.String. Everywhere the old code checked""or relied on== "", decide explicitly what null means, and make sure you send the API the same thing SDKv2 sent (usually: omit the field when null).GetOk - Keep the 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.id - 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 — treat that as a signal the resource may be in the do-not-migrate bucket.
StateUpgrader
使用中的对照表转换Schema和CRUD操作。避免破坏性变更的规则:
references/schema-mapping.md- 块保持为块。用户配置中使用编写的SDKv2块,必须转换为Framework的Block(
Elem: &schema.Resource{...}/schema.ListNestedBlock)——将其转换为嵌套属性会改变用户必须使用的HCL语法,属于破坏性变更。嵌套属性仅适用于新Schema。SetNestedBlock - 空值不等于零值。SDKv2的对未设置的值返回
d.Get("name");Framework模型返回"",可区分空值、未知值和types.String。旧代码中所有检查""或依赖== ""的地方,需明确空值的含义,并确保发送给API的内容与SDKv2一致(通常:空值时省略字段)。GetOk - 保留属性。全新的Framework资源可省略冗余的
id,但迁移的资源必须保留完全一致的Schema——删除或重命名属性会破坏现有状态和配置。id - 状态必须可往返转换。Framework会读取SDKv2写入的状态。若每个属性的名称和类型保持不变,则无需升级状态。若旧Schema存储的值被新类型包以不同方式标准化,则需要——这表明该资源可能属于无需迁移的范畴。
StateUpgrader
Step 4: Move the Registration
步骤4:迁移注册信息
Register the resource in the Framework provider's and delete
it from the SDKv2 provider's in the same commit — mux errors
on duplicates.
Resources()ResourcesMap在Framework Provider的中注册该资源,并在同一提交中删除SDKv2 Provider的中的对应条目——混合服务器会因重复项报错。
Resources()ResourcesMapStep 5: Verify
步骤5:验证
- The pre-existing acceptance tests pass without modification —
especially , which diffs imported state against stored state and catches most null-vs-zero regressions.
ImportStateVerify - 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 this is a two-step test using
terraform-plugin-testingfor the old version, thenExternalProviderswithProtoV6ProviderFactoriesasserting an empty plan. TheConfigPlanChecksskill (if available) documents the pattern.provider-test-patterns - against a real pre-migration state file shows no diff.
terraform plan
- 预先存在的验收测试无修改通过——尤其是,它会比较导入状态与存储状态,可捕获大多数空值与零值的回归问题。
ImportStateVerify - 添加状态兼容性步骤:使用最新发布的(SDKv2)Provider版本应用配置,然后使用迁移后的构建版本执行计划——计划必须为空。在中,这是一个两步测试:使用
terraform-plugin-testing指定旧版本,再使用ExternalProviders并通过ProtoV6ProviderFactories断言计划为空。ConfigPlanChecks技能(若可用)记录了该模式。provider-test-patterns - 针对真实的迁移前状态文件执行,显示无差异。
terraform plan
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; kept
id - 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
- 资源足够简单可迁移(无复杂差异定制),或存在明确的迁移需求
- 混合服务器已运行两种插件;两个插件的Provider级Schema完全一致
- 验收测试在迁移前已存在,迁移后无修改通过
- 块保持为块;属性名称和类型未变更;保留了
id - 空值/省略语义已保留(API接收的内容与SDKv2一致)
- 同一变更中已删除SDKv2的注册信息
- 针对旧版本写入的状态验证了空计划
- 若仓库跟踪发布说明,已添加变更日志条目
Related Skills
相关技能
Use the skill (if available) for Framework CRUD,
finder, and waiter patterns in the ported code, and
for the regression and version-upgrade test patterns.
provider-resourcesprovider-test-patterns迁移后的代码可使用技能(若可用)获取Framework的CRUD、查找器和等待器模式,使用技能获取回归测试和版本升级测试模式。
provider-resourcesprovider-test-patterns