Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Adopting Terraform Plugin Framework

Avatar for nasa9084 nasa9084
February 23, 2026

Adopting Terraform Plugin Framework

at Road To SRE NEXT 2026 @札幌

Avatar for nasa9084

nasa9084

February 23, 2026
Tweet

More Decks by nasa9084

Other Decks in Programming

Transcript

  1. $ whoami • @nasa9084 • LINEϠϑʔגࣜձࣾ Service Embedded SRE Division

    • LYP Premium, LINE Wallet, LINE Sticker/Emoji/Theme Shop • Terraform Provider for Internal Infrastructure • Go, Kubernetes, Shellscript, Yak Shaving Engineer • Hobbies: • g nasa9084/go-switchbot, nasa9084/go-openapi • k kubernetes/website • Playing music, Cooking, Indoor Gardening, Hand-craft, ...
  2. Terraform Plugin • ≒Terraform Provider (as of 2026.02) • Written

    in Go/gRPC • Terraform Plugin SDKv2 (Legacy) • Terraform Plugin Framework • Major public providers are still using SDKv2 • Google Cloud, Azure, GitHub, ... ; Contribution Chance!!!
  3. Bene fi ts of Terraform Plugin Framework • More access

    to raw con fi gurations, plans, and states • Limited access to meaningless con fi gurations, plans, and states • e.g. no prior state on Create, no plan on Delete • Well-typed data models: panic-safe • Extensible type system, extensible validations • Terraform plugin protocol version6: support nested attributes
  4. SDKv2-based Provider Implementation func resourceExample() *schema.Resource { // this type

    is also for data sources; some attrs are for resources, some are datasources return &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, // untyped in Go type system Required: true, }, }, CreateContext: resourceExampleCreate, // each functionaility is just attributes; cannot get error from compiler } } func resourceExampleCreate(_ context.Context, d *schema.ResourceData, meta any) error { client := meta.(*Client) // type assersion; panicable name := d.Get("name").(string) // type assersion; panicable example := client.CreateExample(name) d.Set("description", example.Description) // schema.ResourceData is merged plan or state value }
  5. Framework-based Provider Implementation type resourceExample struct { client *Client //

    typed } type resourceExampleModel struct { Name string `tfsdk:"name"` } func (*resourceExample) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ // typed Required: true, }, }, } } // each functionality is method; can get compiler error if something is missed func (r *resourceExample) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data resourceExampleModel req.Plan.Get(ctx, &data) // we know the type; panic-safe example := r.client.GetExample(data.Name) // no type assersion; panic-safe req.State.Set(ctx, &example) }
  6. SDKv2-based Implementation: Data Access func ExampleCreate(_ context.Context, d *schema.ResourceData, _

    any) diag.Diagnostics { v := d.Get("...") // plan; no access to raw configuration. note that v is any old, new := d.GetChange("...") // old == nil, new == value with Get d.HasChange("...") // always true d.Set("...") // saved into new state } func ExampleRead(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // prior state old, new := d.GetChange("...") // no changes as only prior state is available d.HasChange("...") // always false d.Set("...") // saved into new state } func ExampleUpdate(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // plan; no access to raw configuration old, new := d.GetChange("...") // prior state and plan unless unknown d.HasChange("...") // comparison of prior state and plan d.Set("...") // saved into new state } func ExampleDelete(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // prior state old, new := d.GetChange("...") // no changes as only prior state is available d.HasChange("...") // always false d.Set("...") // extraneous, resource destroy leaves no state }
  7. Framework-based Implementation: Data Access func (_ ExampleResource) Create(_ context.Context, req

    resource.CreateRequest, resp *resource.CreateResponse) { req.Config // raw configuration req.Plan // plan // No req.State as it is always nil resp.State // new state to save } func (_ ExampleResource) Read(_ context.Context, req resource.ReadRequest, resp *resource.CreateResponse) { // No req.Config as configuration cannot be read by provider during read // No req.Plan as there is no plan during read req.State // prior state resp.State // new state to save } func (_ ExampleResource) Update(_ context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { req.Config // raw configuration req.Plan // plan req.State // prior state resp.State // new state data to save } func (_ ExampleResource) Delete(_ context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // No req.Config as configuration cannot be read by provider during delete // No req.Plan as it is always null req.State // prior state resp.State // only available to explicitly remove on error }
  8. Migration from SDKv2 to Framework: Add Provider type frameworkProvider struct

    {} type frameworkProviderModel struct { APIToken types.String `tfsdk:"api_token"` } func NewFrameworkProvider() provider.Provider { return &frameworkProvider{} } func (p *frameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ // Provider-level configuration schema: has to be the same with SDKv2's one. Attributes: map[string]schema.Attribute{ "api_token": schema.StringAttribute{ Description: "API token to call...", }, }, } } func (p *frameworkProvider) Configure(_ context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { var data frameworkProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) // Do some initialization, e.g. create API client based on the provider-level configuration c := NewClient(data.APIToken) resp.ResourceData = c resp.DataSourceData = c }
  9. Migration from SDKv2 to Framework: Add Mux func NewFactory(ctx context.Context)

    (func() ProviderServer, error) { // SDKv2-based provider is v5. Need to upgrade to use new features in the Framework-based provider. sdkServer, _ := tf5to6server.UpgradeServer( ctx, NewSDKv2Provider().GRPCProvider, // The old SDKv2-based provider. ) // Mux muxServer, err := tf6muxserver.NewMuxServer( ctx, providerserver.NewProtocol6(NewFrameworkProvider()), // The new Framework-based provider. func() tfprotov6.ProviderServer{ return sdkServer }, ) return muxServer.ProviderServer, nil }
  10. Migration from SDKv2 to Framework: Implement Framework-based Resources/DataSources var _

    resource.ResourceWithConfigure = &resourceExample{} type resourceExample struct { client *Client } type resourceExampleModel struct {} func NewResourceExample() resource.Resource func (*resourceExample) Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) func (*resourceExample) Schema(context.Context, resource.SchemaRequest, *resource.SchemaResponse) func (*resourceExample) Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) func (*resourceExample) Create(context.Context, resource.CreateRequest, *resource.CreateResponse) func (*resourceExample) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) func (*resourceExample) Update(context.Context, resource.UpdateRequest, *resource.UpdateResponse) func (*resourceExample) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse)
  11. Migration from SDKv2 to Framework: Implement Migration Tests func TestMigrationResourceExample(t

    *testing.T) { resource.Test(t, resource.TestCase{ Steps: []resource.TestStep{ { // Step 1: Apply a configuration with the SDKv2-based provider. ProviderFactories: testSDKv2ProviderFactories(), ConfigDirectory: config.TestNameDirectory(), }, { // Step 2: Apply the same configuration with the Framework-based provider. ProviderFactories: testFrameworkProviderFactories(), ConfigDirectory: config.TestNameDirectory(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), // There should be no change if they're compatible. }, }, }, }, }) }
  12. Migrating from SDKv2 to Framework: DataSource Migration Tests // Use

    built-in terraform_data resource data "example_datasource" "test" { // ... } resource "terraform_data" "test" { input = data.example_datasource.test }
  13. Migration from SDKv2 to Framework: Register to the Framework-based Provider

    func (p *frameworkProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewResourceExample, } } func (p *frameworkProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewDataSourceExample, } }
  14. Migration from SDKv2 to Framework: Unregister from SDKv2-based Provider func

    SDKv2Provider() *schema.Provider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ // "example_resource": resourceExample(), }, DataSourcesMap: map[string]*schema.Resource{ // "example_datasource": datasourceExample(), }, } }
  15. Issues during Migration: Optional-Computed Nested Attributes • Set/List were utilized

    to achieve "nested attributes" in SDKv2 • The equivalent is Blocks but: not support Optional-Computed • Use Nested Attribute if breaking changes are acceptable • Unknown/empty value handling • You might need to add some workaround
  16. Q?