provider-framework-migration について
Terraform Provider のリソースを Plugin SDKv2 から Plugin Framework へ段階的に移行するスキルです。両実装の mux と、スキーマ、state、動作の互換性維持に重点を置きます。
主な機能
- 01SDKv2 と Framework の mux
- 02リソース単位の移行手順
- 03スキーマ対応の指針
- 04null とゼロの挙動差への対処
- 05state 互換性テスト
活用シーン
- 01既存リソースの段階的な移行
- 02移行中の設定互換性の維持
- 03旧バージョンが書き込んだ state の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hashicorp/ agent-skills --skill provider-framework-migrationインストール後、Claude Code で/provider-framework-migrationを入力して読み込まれたか確認します。
同梱ファイルreferences/schema-mapping.md
詳細
Plugin SDKv2 から Plugin Framework への移行
新規リソースとデータソースには Plugin Framework が必要です。SDKv2 はメンテナンスのみを対象としています。移行はリソースごとに段階的に進めます。マルチプレクサーを使うプロバイダーでは、SDKv2 と Framework の実装を並行して提供できるため、一度にすべてを書き換える必要はありません。このスキルでは、マルチプレクサーの設定、リソースごとのワークフロー、および機械的な置き換えが気づかれにくい破壊的変更につながる落とし穴について説明します。
参考資料(必要に応じて読み込んでください):
references/schema-mapping.md— SDKv2 から Framework への置き換えをコード例とともに示した詳細な表
公式ガイド:Framework の移行。
移行すべきかどうかを判断する
移行には実際のリスクがあり、ユーザーから見えるメリットはほとんどないため、まず優先度を判断します。
- きっかけとなる必要性(Framework でしか使えない機能、SDKv2 では修正できないバグ)がない限り、複雑なリソースや広く使われているリソースは移行しないでください。2 つの SDK には動作上の違いがあり、特に null 値とゼロ値の扱いの違いは、既存ユーザーにとって破壊的変更として表面化します。これは terraform-provider-aws のような大規模プロバイダーで採用されている方針です。
- シンプルなリソースは安全に移行できます。フラットなスキーマで、
DiffSuppressFunc、CustomizeDiff、StateFunc、複雑なネストブロックがないものが対象です。 - 新しい機能を追加するために古いコードを移行する必要はありません。マルチプレクサーを設定し、新しいリソースは Framework で、既存のリソースと並行して作成してください。
プロバイダーがどのモードかを確認するには、go.mod を調べます。terraform-plugin-mux があれば、すでに両方を提供しています。terraform-plugin-sdk/v2 のみであれば SDKv2 のみであり、最初にマルチプレクサーを設定します。terraform-plugin-framework のみであれば、移行は完了しています。
ステップ 1:プロバイダーにマルチプレクサーを設定する
main.go で両方のプラグインサーバーを結合します。プロトコルバージョン 6 を提供するには、tf5to6server を使って SDKv2 サーバーをアップグレードします(プロトコル 6 には Terraform CLI >= 1.0 が必要です。0.12 以降もサポートする必要がある場合は、代わりに tf6to5server/tf5muxserver を使ってプロトコル 5 でマルチプレクサーを設定してください。ただし、その場合、Framework プロバイダーではネスト属性などのプロトコル 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)
}
}
実運用で問題になりやすいマルチプレクサーの要件:
- プロバイダーのスキーマは、両方のプラグイン間で完全に一致している必要があります。プロバイダーレベルの属性、型、説明をすべて揃えてください。プロバイダー設定の情報源は 1 つに保ち、もう一方にも反映します。
- 各リソースとデータソースは、2 つのプラグインのうちどちらか一方にのみ存在できます。移行の最後の手順は、SDKv2 の登録を削除することです。
- プロトコル 6 で Registry に公開する場合は、
terraform-registry-manifest.jsonに"metadata": {"protocol_versions": ["6.0"]}を設定します。
ステップ 2:変更を加える前にベースラインを確立する
移行後のリソースは、ユーザーから見て移行前と区別できない状態でなければなりません。移行前に用意したテストで、それを実証してください。
- リソースに合格済みの受け入れテストがあることを確認します。
ImportStateVerify: trueを指定した_basic、_disappears、および属性ごとの更新テストが必要です。テストが不足している場合は、まず SDKv2 の実装に対して作成してください。これらのテストが移行の受け入れ基準となり、移行後も変更せずに合格しなければなりません。 - テストでは捉えられない動作を確認します。属性のデフォルト値、省略されたオプション属性の扱い(null と
""/0/falseのどれになるかが重要です)、およびDiffSuppressFunc/StateFuncによる正規化などです。
ステップ 3:リソースを移植する
references/schema-mapping.md のマッピング表を使って、スキーマと CRUD を変換します。破壊的変更を防ぐルールは次のとおりです。
- ブロックはブロックのままにします。 ユーザー設定で
block { ... }構文として記述される SDKv2 のElem: &schema.Resource{...}は、Framework の Block(schema.ListNestedBlock/SetNestedBlock)に変換する必要があります。ネストされた属性に変換すると、ユーザーが記述する HCL 構文が変わり、破壊的変更になります。ネストされた属性は新しいスキーマにのみ使用します。 - null はゼロ値ではありません。 SDKv2 の
d.Get("name")は未設定の場合に""を返していました。Framework のモデルではtypes.Stringを使い、null、unknown、""を区別します。以前のコードで== ""をチェックしていた箇所や、GetOkに依存していた箇所では、null の意味を明示的に判断し、API に SDKv2 と同じ値を送信するようにします(通常は、null の場合にフィールドを省略します)。 id属性は残します。 新規の Framework リソースでは冗長なidを省略してもかまいませんが、移行済みリソースではスキーマを完全に維持する必要があります。属性を削除または名前変更すると、既存の状態や設定が壊れます。- 状態を往復できるようにします。 Framework は SDKv2 が書き込んだ状態を読み取ります。すべての属性で名前と型が維持されていれば、状態のアップグレードは不要です。古いスキーマで保存されていた値を新しい型パッケージが異なる方法で正規化する場合は、
StateUpgraderが必要です。これは、そのリソースが移行対象外にすべきものかもしれないという合図として扱います。
ステップ 4:登録を移す
Framework プロバイダーの Resources() にリソースを登録し、同じコミットで SDKv2 プロバイダーの ResourcesMap から削除します。重複すると mux がエラーになります。
ステップ 5:検証する
- 既存の受け入れテストが変更なしで通ることを確認します。特に
ImportStateVerifyは、インポートした状態と保存済みの状態を比較し、null とゼロ値に関するリグレッションの大半を検出します。 - 状態互換性の手順を追加します。最後にリリースされた(SDKv2)プロバイダーのバージョンで設定を適用し、移行後のビルドでプランを作成します。プランは空でなければなりません。
terraform-plugin-testingでは、旧バージョンにExternalProvidersを使い、次にConfigPlanChecksで空のプランをアサートするProtoV6ProviderFactoriesを使う 2 段階のテストになります。利用できる場合は、provider-test-patternsスキルにこのパターンが記載されています。 - 移行前の実際の状態ファイルに対して
terraform planを実行し、差分が表示されないことを確認します。
チェックリスト
- リソースが移行に十分なほど単純である(複雑な差分カスタマイズがない)、または移行を進める必要性がある
- mux が両方のプラグインを提供し、プロバイダーレベルのスキーマが両方で同一である
- 受け入れテストが移行前から存在し、移行後も変更なしで通る
- ブロックがブロックのまま維持され、属性名と型が変更されず、
idが保持されている - null/省略時のセマンティクスが維持されている(API に SDKv2 と同じ値が送信される)
- 同じ変更内で SDKv2 の登録が削除されている
- 前回のリリースで書き込まれた状態に対して、空のプランが確認されている
- リポジトリでリリースノートを管理している場合、変更履歴が追加されている
関連スキル
移植後のコードで Framework の CRUD、finder、waiter のパターンを使う際は provider-resources スキル(利用できる場合)を、リグレッションテストとバージョンアップグレードテストのパターンには provider-test-patterns を使います。