从 Plugin SDKv2 迁移到 Plugin Framework 的技能说明。
背景与适用前提
Plugin Framework 是新资源与数据源的必需选择,SDKv2 仅做维护。迁移按资源、增量进行:一个 muxed provider 可同时提供 SDKv2 与 Framework 实现,因此无需大爆炸式重写。该技能覆盖 mux 设置、单资源工作流,以及会把机械翻译变成静默破坏性变更的行为陷阱。
参考资料(需要时加载):references/schema-mapping.md,包含完整的 SDKv2 → Framework 翻译对照表与代码对。官方指南:Framework migration。
第一步:是否迁移
迁移有真实风险且用户可见收益很小,因此先分诊:
- 不要在没有驱动需求(Framework 独有功能、SDKv2 无法修复的 bug)的情况下迁移复杂或高频使用的资源。两套 SDK 行为不同,最重要的是 null 与零值的差异,这些差异会以破坏性变更形式暴露给现有用户。这是 terraform-provider-aws 等大型 provider 的既定政策。
- 简单资源可安全迁移:扁平 schema、无 DiffSuppressFunc、无 CustomizeDiff、无 StateFunc、无复杂嵌套块。
- 新能力从不需要迁移旧代码——mux 后在旧资源旁边用 Framework 写新资源即可。
判断 provider 处于哪种模式,查看 go.mod:存在 terraform-plugin-mux 表示已同时提供两者;只有 terraform-plugin-sdk/v2 表示仅 SDKv2(mux 设置是第一步);只有 terraform-plugin-framework 表示迁移完成。
第二步:Mux provider
在 main.go 中合并两个插件服务器。服务协议版本 6 需要用 tf5to6server 升级 SDKv2 服务器(协议 6 需要 Terraform CLI >= 1.0;若必须支持 0.12+,则用 tf6to5server/tf5muxserver 在协议 5 上 mux,但此时 Framework provider 无法使用仅协议 6 的功能,如嵌套属性)。示例代码:
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 要求:
- 两个插件的 provider schema 必须完全一致——相同的 provider 级属性、相同类型、相同描述。为 provider 配置保留单一事实来源并镜像它。
- 每个资源与数据源只能存在于两个插件之一。迁移的最后一步是删除 SDKv2 注册。
- 若以协议 6 发布到 Registry,在 terraform-registry-manifest.json 中设置 "metadata": {"protocol_versions": ["6.0"]}。
第三步:动手前先建立基线
迁移后的资源对用户必须不可区分。用迁移前就存在的测试来证明:
- 确保资源有通过的验收覆盖:带导入步骤的 _basic(ImportStateVerify: true)、_disappears,以及逐属性更新测试。若覆盖缺失,先针对 SDKv2 实现编写——这些测试是迁移的验收标准,之后必须原样通过。
- 注意测试无法捕获的行为:属性默认值、可选属性省略时发生什么(null 与 ""/0/false 即将变得重要)、任何 DiffSuppressFunc/StateFunc 规范化。
第四步:移植资源
使用 references/schema-mapping.md 中的映射表翻译 schema 与 CRUD。防止破坏性变更的规则:
- Block 保持为 block。用户配置中写成 block { ... } 语法的 SDKv2 Elem: &schema.Resource{...} 必须变为 Framework Block(schema.ListNestedBlock/SetNestedBlock)——把它转成嵌套属性会改变用户必须编写的 HCL 语法,这是破坏性变更。嵌套属性仅用于新 schema。
- Null 不是零。SDKv2 的 d.Get("name") 在未设置时返回 "";Framework 模型给出的 types.String 能区分 null、unknown 和 ""。凡是旧代码检查 == "" 或依赖 GetOk 的地方,都要明确决定 null 的含义,并确保发给 API 的内容与 SDKv2 相同(通常是 null 时省略该字段)。
- 保留 id 属性。全新的 Framework 资源可以省略冗余的 id,但迁移的资源必须保留其确切 schema——删除或重命名属性会破坏现有 state 与配置。
- State 必须回环。Framework 会读取 SDKv2 写入的 state。若每个属性保持名称与类型,则无需 state 升级。若旧 schema 存储的值被新 types 包以不同方式规范化,则需要 StateUpgrader——将其视为该资源可能属于“不要迁移”类别的信号。
第五步:移动注册
在 Framework provider 的 Resources() 中注册该资源,并在同一次提交中从 SDKv2 provider 的 ResourcesMap 删除它——mux 遇到重复会报错。
第六步:验证
- 预先存在的验收测试原样通过——尤其是 ImportStateVerify,它会将导入的 state 与存储的 state 做 diff,能捕获大多数 null-vs-zero 回归。
- 增加 state 兼容性步骤:用最后发布的(SDKv2)provider 版本 apply 一个配置,然后用迁移后的构建 plan——plan 必须为空。在 terraform-plugin-testing 中这是一个两步测试,用 ExternalProviders 指定旧版本,然后以 ProtoV6ProviderFactories 配合 ConfigPlanChecks 断言空 plan。provider-test-patterns 技能(若可用)记录了该模式。
- 针对真实迁移前 state 文件执行 terraform plan,不显示 diff。
检查清单
- 资源足够简单可迁移(无复杂 diff 定制),或有明确驱动需求
- Mux 同时提供两个插件;两者的 provider 级 schema 一致
- 验收测试在迁移前已存在,迁移后原样通过
- Block 仍为 block;属性名称与类型未变;保留 id
- Null/省略语义得到保留(API 收到的与 SDKv2 相同)
- 在同一变更中移除 SDKv2 注册
- 针对上一发布版写入的 state 验证空 plan
- 若仓库跟踪发布说明,添加 changelog 条目
相关技能
移植后的代码中的 Framework CRUD、finder 和 waiter 模式可参考 provider-resources 技能(若可用),回归与版本升级测试模式可参考 provider-test-patterns。