| name | tf-resource-migration |
| description | Migrate existing Terraform resources from SDK provider (sdkprovider) to Plugin Framework. Use when converting legacy resources, ensuring state compatibility, or when the user asks to port/migrate existing resources. |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write, Task |
Terraform Resource Migration Skill
Migrate existing SDK-based resources to Plugin Framework while maintaining state compatibility and behavior parity.
Tooling Rule (read first)
Always drive generation, formatting, and linting through Task:
task generate — generates code and runs task fmt automatically at the end
task fmt — formats Go, Terraform, and whitespace
task lint — runs all linters
Do NOT use go run ./generators/..., go generate, gofmt, goimports, golangci-lint, or make for these workflows. See AGENTS.md and Taskfile.dist.yml (run task --list) for the full command surface.
Overview
This skill guides migration of resources from:
- Source:
internal/sdkprovider/ (terraform-plugin-sdk/v2)
- Target:
internal/plugin/ (terraform-plugin-framework) with YAML-generated code
Key Challenge: Preserve exact behavior and state compatibility so users don't experience breaking changes.
Prerequisites: This skill builds on tf-resource-generator. For YAML syntax, adapter API, modifier patterns, custom view overrides, write-only fields, and all implementation details, see that skill.
When to Use This Skill
Use this skill when:
- Migrating existing
aiven_* resources from SDK to Plugin Framework
- User says: "migrate", "convert", "port", "move to Plugin Framework"
Use tf-resource-generator skill when:
- Creating brand new resources from scratch
- Questions about YAML syntax, adapter API, or implementation patterns
Migration Workflow
1. Analyze the Existing SDK Resource
Find the SDK resource:
find internal/sdkprovider -name "*resource_*.go" | grep -i "resource_name"
find internal/sdkprovider -name "*datasource_*.go" | grep -i "resource_name"
Read and document:
- Schema definition (all fields, types, attributes)
- CRUD functions (Create, Read, Update, Delete)
- Custom logic and transformations
- State upgrade functions (if any)
- Existing tests (critical for parity)
2. Identify API Operations
Check what API operations the SDK resource uses:
grep -A 5 "client\." internal/sdkprovider/service/resource_name.go
grep -A 5 "client\." internal/sdkprovider/service/resource_name_data_source.go
Then find corresponding OpenAPI operation IDs. See tf-resource-generator for OpenAPI search patterns.
Determine clientHandler: The clientHandler YAML value is the Go package name under github.com/aiven/go-client-codegen/handler/. Find it by searching for the operation ID in the module cache:
grep -r "OperationID" $(go env GOMODCACHE)/github.com/aiven/go-client-codegen*/handler/
For example, ServiceFlinkCreateApplication lives in handler/flinkapplication/, so clientHandler: flinkapplication.
3. Map SDK Schema to YAML
SDK Type -> YAML Type
| SDK Type | YAML Type |
|---|
schema.TypeString | type: string |
schema.TypeInt | type: integer |
schema.TypeFloat | type: number |
schema.TypeBool | type: boolean |
schema.TypeList | type: arrayOrdered |
schema.TypeSet | type: array (or arrayOrdered for performance) |
schema.TypeMap | additionalProperties: {type: string} |
SDK Attributes -> YAML Attributes
| SDK Attribute | YAML Attribute |
|---|
Required: true | required: true |
Optional: true | optional: true |
Computed: true | computed: true |
Sensitive: true | sensitive: true |
ForceNew: true | forceNew: true |
ConflictsWith: [] | conflictsWith: [] |
ExactlyOneOf: [] | exactlyOneOf: [] |
Nested Blocks
SDK Set of objects:
"tags": {
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {Type: schema.TypeString},
"value": {Type: schema.TypeString},
},
},
}
YAML (use arrayOrdered for performance):
schema:
tags:
type: arrayOrdered
items:
type: object
properties:
key:
type: string
value:
type: string
4. Preserve ID Structure
CRITICAL: The ID format MUST stay the same for state compatibility.
Find the ID format in SDK code:
grep -A 2 "SetId" internal/sdkprovider/service/resource_name.go
grep -B 5 "buildResourceID\|parseResourceID" internal/sdkprovider/service/resource_name.go
Common ID patterns:
- Single field:
project
- Composite:
project/service_name/database_name
Set in YAML:
idAttributeComposed: [project, service_name, database_name]
5. Handle Custom Logic
Identify in SDK code and map to generator features:
| SDK Pattern | Generator Feature |
|---|
StateUpgraders | May need version in YAML + state upgrader |
CustomizeDiff | planModifier: true or ModifyPlan |
| Flatten/Expand functions | expandModifier: true / flattenModifier: true |
DiffSuppressFunc | planModifier: true |
| Multiple API calls in Update | Custom updateView via init() override |
| Complex delete (cancel + delete) | Custom deleteView via init() override |
| Data source looks up by alt key (e.g. name) | A second read op with datasourceLookup: true + resultListLookupKeys; add resultIDField: <GoField> when the lookup endpoint differs in shape from the canonical read and should only resolve the id |
| Sensitive field not stored in state | writeOnly: true |
For implementation details of each, see tf-resource-generator skill.
6. Create YAML Definition
Create definitions/aiven_resource_name.yml. For complete YAML syntax reference, see tf-resource-generator skill.
IMPORTANT: Definition files MUST have the aiven_ prefix. The filename becomes the resource name directly.
Focus on migration-specific concerns:
- Match all SDK schema fields exactly
- Preserve ID structure
- Copy descriptions from SDK resource
- Keep every SDK
Sensitive field as sensitive: true in YAML (no regressions)
7. Generate and Build
task generate
task build
task lint
8. State Compatibility Verification
CRITICAL: Ensure state is compatible between SDK and Plugin Framework versions.
Check schema version in SDK:
grep -A 3 "SchemaVersion" internal/sdkprovider/service/resource_name.go
If SDK has SchemaVersion > 0, you MUST handle state upgrades.
9. Backward Compatibility Testing
CRITICAL: Test that existing state from SDK version works with Plugin Framework version.
Use acc.BackwardCompatibilitySteps() helper:
func TestAccAivenResource_backwardCompat(t *testing.T) {
resourceName := "aiven_resource_name.test"
projectName := acc.ProjectName()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acc.TestAccPreCheck(t) },
Steps: acc.BackwardCompatibilitySteps(t, acc.BackwardCompatConfig{
TFConfig: testAccResourceConfig(projectName),
OldProviderVersion: "4.47.0",
Checks: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "project", projectName),
),
}),
})
}
Find the latest version:
head -20 CHANGELOG.md
What this test does:
- Creates resource with OLD SDK provider version
- Applies with NEW Plugin Framework version
- Verifies state is compatible and attributes match
Examples:
internal/plugin/service/mysql/database/database_test.go - Basic backward compatibility
internal/plugin/service/pg/user/user_test.go - Complex resource with custom update logic
10. Parity Testing
CRITICAL: Verify behavior matches SDK resource exactly.
Find SDK tests:
ls internal/sdkprovider/service/*resource_name*_test.go
Ensure Plugin Framework tests cover:
- All CRUD operations from SDK tests
- Update step: If the resource implements
Update (mutable fields that are not all ForceNew), acceptance tests MUST include a Terraform apply that changes at least one updatable attribute after create — i.e. an explicit update step in the test sequence, not only create + destroy. Skip this only when the resource is create/delete-only or every mutable change forces replacement.
- Edge cases
- Error handling
- Import functionality
- Special field behaviors
State Compatibility Checklist
Before marking migration complete:
Common Migration Issues
| Issue | Solution |
|---|
| Sensitive field no longer marked sensitive | Set sensitive: true on the attribute (and nested fields if applicable); match SDK Sensitive: true exactly |
| ID format changed accidentally | Verify idAttributeComposed matches SDK's ID builder |
| Set ordering causes diffs | Use arrayOrdered instead of array |
| Computed field becomes required | Keep as computed: true if API provides it |
| Custom validation lost | Implement in custom modifier or use schema validation |
| State upgrade needed | Implement state upgrader in Plugin Framework |
| DiffSuppressFunc behavior | Use planModifier: true for custom diff logic |
| Renamed ID field missing in old state | Use planModifier: true to extract from composite ID |
| Read fails with 404 after migration | Likely a renamed ID field is empty — use planModifier |
| "was null, but now cty.X" error | Add computed: true + useStateForUnknown: true (see generator skill) |
| Field nested differently in API | Use expandModifier and flattenModifier (see generator skill) |
| Multiple update operations needed | Override updateView via init() (see generator skill) |
| Data source lookup key differs | Add a second read operation with datasourceLookup: true + resultListLookupKeys. If the lookup endpoint returns a different shape than the canonical read (e.g. a directory of ids), also set resultIDField: <GoField> (see generator skill) |
Migration-Specific Commands
find internal/sdkprovider -name "*resource_*.go" | grep -i "name"
grep -A 20 "Schema:" internal/sdkprovider/service/resource.go
grep -A 2 "SetId" internal/sdkprovider/service/resource.go
diff internal/sdkprovider/service/resource.go internal/plugin/service/resource/zz_resource.go
task test-acc -- -run TestAccAivenResource_backwardCompat
After Migration
Once all tests pass and state compatibility is verified:
- Remove SDK resource - Delete from
internal/sdkprovider/ and remove provider registration
- Update documentation - Ensure docs reflect the Plugin Framework version
- Add migration notes if needed - Document any unavoidable behavioral differences
- Add changelog entry - Add record to
CHANGELOG.md under the unreleased section:
- Migrate `aiven_resource_name` to the Plugin Framework
- Change `aiven_resource_name`: deprecate `termination_protection` field. Instead, use [prevent_destroy](https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion)
Do not maintain both versions - this creates maintenance burden and user confusion.
Key Principles
- State compatibility first - Users should not need to recreate resources
- Preserve exact behavior - Match SDK resource behavior precisely
- Preserve sensitivity - All fields that were
Sensitive in SDK must stay sensitive in Plugin Framework (sensitive: true); never expose secrets in plan or state output by omission
- Test thoroughly - All SDK test scenarios must pass with Plugin version; when Update exists, tests must exercise it via an update apply step
- Remove SDK version - Once verified, delete SDK resource to avoid maintenance burden