provider-framework-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrating 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):
  • references/schema-mapping.md
    — the full SDKv2 → Framework translation table with code pairs
Official guide: Framework migration.
Plugin Framework是新增资源和数据源的必备工具;SDKv2仅用于维护。迁移采用逐资源、增量式方式:混合Provider可同时运行SDKv2和Framework实现,因此无需一次性重写全部代码。本技能涵盖混合服务器搭建、逐资源迁移流程,以及可能导致隐性破坏性变更的行为陷阱。
参考资料(按需查阅):
  • references/schema-mapping.md
    — 完整的SDKv2 → Framework转换对照表,包含代码示例对
官方指南: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
    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.
迁移存在实际风险,且用户可见收益有限,因此需先进行优先级划分:
  • 若无明确需求(如仅Framework支持的功能、SDKv2无法修复的Bug),请勿迁移复杂或高频使用的资源。两个SDK在行为上存在差异——最关键的是空值与零值的处理差异——这些差异会对现有用户造成破坏性变更。这是terraform-provider-aws等大型Provider的既定策略。
  • 简单资源可安全迁移:扁平化Schema、无
    DiffSuppressFunc
    、无
    CustomizeDiff
    、无
    StateFunc
    、无复杂嵌套块。
  • 新增功能无需迁移旧代码——可在Framework中编写新资源,与旧资源通过混合方式共存。
要判断Provider所处模式,可查看
go.mod
:若存在
terraform-plugin-mux
,说明已同时运行两种插件;仅存在
terraform-plugin-sdk/v2
则为纯SDKv2模式(第一步需搭建混合服务器);仅存在
terraform-plugin-framework
则表示迁移已完成。

Step 1: Mux the Provider

步骤1:搭建混合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):
go
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
    .
main.go
中合并两个插件服务器。要支持协议版本6,需使用
tf5to6server
升级SDKv2服务器(协议6要求Terraform CLI >= 1.0;若必须支持0.12+,则需使用
tf6to5server
/
tf5muxserver
在协议5下混合——但此时Framework Provider无法使用仅协议6支持的功能,如嵌套属性):
go
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:
  1. Ensure the resource has passing acceptance coverage:
    _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.
  2. 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.
迁移后的资源对用户而言必须无差异。在迁移前通过测试验证这一点:
  1. 确保资源有通过的验收测试覆盖:包含导入步骤的
    _basic
    测试(
    ImportStateVerify: true
    )、
    _disappears
    测试,以及逐属性更新测试。若测试覆盖不足,需先针对SDKv2实现编写测试——这些测试是迁移的验收标准,迁移后必须无修改通过
  2. 记录测试未覆盖的行为:属性默认值、省略可选属性时的表现(空值与
    ""
    /
    0
    /
    false
    的区别即将变得重要),以及任何
    DiffSuppressFunc
    /
    StateFunc
    的标准化处理。

Step 3: Port the Resource

步骤3:迁移资源

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.
使用
references/schema-mapping.md
中的对照表转换Schema和CRUD操作。避免破坏性变更的规则:
  • 块保持为块。用户配置中使用
    Elem: &schema.Resource{...}
    编写的SDKv2块,必须转换为Framework的Block
    schema.ListNestedBlock
    /
    SetNestedBlock
    )——将其转换为嵌套属性会改变用户必须使用的HCL语法,属于破坏性变更。嵌套属性仅适用于新Schema。
  • 空值不等于零值。SDKv2的
    d.Get("name")
    对未设置的值返回
    ""
    ;Framework模型返回
    types.String
    ,可区分空值、未知值和
    ""
    。旧代码中所有检查
    == ""
    或依赖
    GetOk
    的地方,需明确空值的含义,并确保发送给API的内容与SDKv2一致(通常:空值时省略字段)。
  • 保留
    id
    属性
    。全新的Framework资源可省略冗余的
    id
    ,但迁移的资源必须保留完全一致的Schema——删除或重命名属性会破坏现有状态和配置。
  • 状态必须可往返转换。Framework会读取SDKv2写入的状态。若每个属性的名称和类型保持不变,则无需升级状态。若旧Schema存储的值被新类型包以不同方式标准化,则需要
    StateUpgrader
    ——这表明该资源可能属于无需迁移的范畴。

Step 4: Move the Registration

步骤4:迁移注册信息

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.
在Framework Provider的
Resources()
中注册该资源,并在同一提交中删除SDKv2 Provider的
ResourcesMap
中的对应条目——混合服务器会因重复项报错。

Step 5: Verify

步骤5:验证

  1. The pre-existing acceptance tests pass without modification — especially
    ImportStateVerify
    , which diffs imported state against stored state and catches most null-vs-zero regressions.
  2. 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.
  3. terraform plan
    against a real pre-migration state file shows no diff.
  1. 预先存在的验收测试无修改通过——尤其是
    ImportStateVerify
    ,它会比较导入状态与存储状态,可捕获大多数空值与零值的回归问题。
  2. 添加状态兼容性步骤:使用最新发布的(SDKv2)Provider版本应用配置,然后使用迁移后的构建版本执行计划——计划必须为空。在
    terraform-plugin-testing
    中,这是一个两步测试:使用
    ExternalProviders
    指定旧版本,再使用
    ProtoV6ProviderFactories
    并通过
    ConfigPlanChecks
    断言计划为空。
    provider-test-patterns
    技能(若可用)记录了该模式。
  3. 针对真实的迁移前状态文件执行
    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;
    id
    kept
  • 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
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.
迁移后的代码可使用
provider-resources
技能(若可用)获取Framework的CRUD、查找器和等待器模式,使用
provider-test-patterns
技能获取回归测试和版本升级测试模式。