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

Adopting Terraform Plugin Framework

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
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?