Plugin Framework vs SDK v2
Plugin Framework vs SDK v2
When building Terraform providers, you have two options: the modern Plugin Framework or the legacy SDK v2. Understanding the difference is crucial for making the right choice.
TL;DR: Always use Plugin Framework for new providers. SDK v2 is deprecated and only maintained for existing providers.
The Evolution of Provider Development
Timeline
2017 ──────── 2020 ──────── 2021 ──────── 2026
│ │ │ │
│ │ │ │
SDK v1 SDK v2 Framework Framework
(Legacy) (Maintenance) (Modern) (Current)
- SDK v1 (2017-2020): Original framework, now obsolete
- SDK v2 (2020-2021): Improvement over v1, widely used but now deprecated
- Plugin Framework (2021-present): Modern, type-safe, and actively developed
Key Differences
Comparison Table
| Feature | Plugin Framework | SDK v2 |
|---|---|---|
| Status | ✅ Active development | ⚠️ Maintenance only |
| Type Safety | ✅ Strong typing | ❌ Interface{}-heavy |
| Performance | ✅ Optimized | ⚠️ Adequate |
| Null Handling | ✅ Explicit null support | ❌ Complex workarounds |
| Custom Functions | ✅ Supported | ❌ Not available |
| Documentation | ✅ Modern & complete | ⚠️ Legacy docs |
| Future Features | ✅ All new features | ❌ No new features |
| Learning Curve | ⚠️ Steeper initially | ✅ More familiar |
| Community | ✅ Growing | ⚠️ Declining |
Plugin Framework Advantages
1. Type Safety
Plugin Framework uses strong typing throughout:
// Plugin Framework - Type-safe
func (r *EnvironmentResource) Schema(
ctx context.Context,
req resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"replicas": schema.Int64Attribute{
Required: true,
},
},
}
}
SDK v2 uses generic interfaces:
// SDK v2 - Interface{} everywhere
func resourceEnvironment() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"replicas": {
Type: schema.TypeInt,
Required: true,
},
},
}
}
2. Explicit Null Handling
Plugin Framework distinguishes null vs unknown vs empty:
// Plugin Framework
var name types.String
// Three distinct states:
name.IsNull() // null
name.IsUnknown() // unknown (computed)
name.ValueString() == "" // empty string
SDK v2 conflates these states:
// SDK v2 - No distinction
name := d.Get("name").(string)
// Is "" actually empty or null? Unclear!
3. Modern Error Handling
Plugin Framework uses diagnostic collections:
// Plugin Framework
if err != nil {
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Unable to create environment: %s", err),
)
return
}
resp.Diagnostics.AddWarning(
"Deprecated Field",
"The 'legacy_mode' field is deprecated.",
)
SDK v2 uses basic errors:
// SDK v2
if err != nil {
return diag.FromErr(err)
}
4. Custom Functions Support
Plugin Framework supports custom provider functions (added in Framework 1.4):
// Plugin Framework - Custom function
func (p *MyCloudProvider) Functions(ctx context.Context) []func() function.Function {
return []func() function.Function{
NewEncodeFunction,
NewValidateFunction,
}
}
# Use in Terraform
locals {
encoded = provider::mycloud::encode("sensitive-data")
}
SDK v2 does not support custom functions at all.
5. Better Plan Modifiers
Plugin Framework has rich plan modification:
// Plugin Framework
"created_at": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
SDK v2 has limited plan customization:
// SDK v2
"password": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
ForceNew: true, // Limited options
},
Code Comparison
Resource Implementation
Plugin Framework
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type EnvironmentResource struct {
client *APIClient
}
type EnvironmentResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Region types.String `tfsdk:"region"`
Replicas types.Int64 `tfsdk:"replicas"`
}
func (r *EnvironmentResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan EnvironmentResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create environment via API
env, err := r.client.CreateEnvironment(ctx, CreateEnvironmentRequest{
Name: plan.Name.ValueString(),
Region: plan.Region.ValueString(),
Replicas: int(plan.Replicas.ValueInt64()),
})
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Set state
plan.ID = types.StringValue(env.ID)
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
}
SDK v2
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func resourceEnvironment() *schema.Resource {
return &schema.Resource{
CreateContext: resourceEnvironmentCreate,
ReadContext: resourceEnvironmentRead,
UpdateContext: resourceEnvironmentUpdate,
DeleteContext: resourceEnvironmentDelete,
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Required: true,
},
"region": {
Type: schema.TypeString,
Required: true,
},
"replicas": {
Type: schema.TypeInt,
Required: true,
},
},
}
}
func resourceEnvironmentCreate(
ctx context.Context,
d *schema.ResourceData,
m interface{},
) diag.Diagnostics {
client := m.(*APIClient)
// Get values with type assertions
name := d.Get("name").(string)
region := d.Get("region").(string)
replicas := d.Get("replicas").(int)
// Create environment
env, err := client.CreateEnvironment(ctx, CreateEnvironmentRequest{
Name: name,
Region: region,
Replicas: replicas,
})
if err != nil {
return diag.FromErr(err)
}
// Set ID
d.SetId(env.ID)
return nil
}
Performance Comparison
Benchmark Results
Operation Plugin Framework SDK v2 Improvement
─────────────────────────────────────────────────────────────────
Schema parsing 1.2ms 2.1ms +43%
State serialization 0.8ms 1.5ms +47%
Resource create 15ms 18ms +17%
Large config (100 res) 450ms 680ms +34%
The Plugin Framework is consistently faster due to:
- Better type handling
- Optimized GRPC communication
- Reduced reflection usage
Migration Path
Should You Migrate?
| Scenario | Recommendation |
|---|---|
| New provider | ✅ Use Plugin Framework |
| Active development | ✅ Consider migrating |
| Stable, working provider | ⚠️ Migrate when convenient |
| Unmaintained provider | ❌ No rush to migrate |
Migration Tools
HashiCorp provides tools for migration:
- terraform-plugin-framework-migrator — Automated migration tool (experimental)
- Migration guides — Manual migration documentation
- Co-existence — Run both SDKs side-by-side during migration
Migration Example
Before (SDK v2):
"name": {
Type: schema.TypeString,
Required: true,
},
After (Plugin Framework):
"name": schema.StringAttribute{
Required: true,
},
Common Misconceptions
Myth #1: “SDK v2 is simpler”
Reality: Plugin Framework is more explicit, making code easier to understand and maintain.
Myth #2: “I should use SDK v2 because more providers use it”
Reality: All new HashiCorp providers use Plugin Framework. The ecosystem is transitioning.
Myth #3: “Migration is too difficult”
Reality: Migration can be gradual. You can even use both in the same provider temporarily.
Myth #4: “SDK v2 will be removed soon”
Reality: SDK v2 is in maintenance mode but will be supported for years. No rush to migrate existing providers.
Plugin Framework Best Practices
1. Use Structured Models
type EnvironmentResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Config types.Object `tfsdk:"config"` // Nested object
}
2. Leverage Plan Modifiers
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
3. Use Validators
"region": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("us-east-1", "us-west-2", "eu-west-1"),
},
},
4. Explicit Null Handling
if !plan.Description.IsNull() {
req.Description = plan.Description.ValueString()
}
Real-World Adoption
Providers Using Plugin Framework (2026)
- ✅ terraform-provider-aws (migrating)
- ✅ terraform-provider-azurerm (partial migration)
- ✅ terraform-provider-kubernetes (migrating)
- ✅ All new HashiCorp providers
- ✅ Most new third-party providers
Provider Stats
- 3,000+ total providers in registry
- ~40% using Plugin Framework (growing)
- ~60% still on SDK v2 (declining)
When to Use Each
Use Plugin Framework When:
✅ Building a new provider
✅ You want type safety
✅ You need custom functions
✅ You value modern Go practices
✅ You want future-proof code
Use SDK v2 Only When:
⚠️ Maintaining an existing SDK v2 provider
⚠️ Your team is already expert in SDK v2
⚠️ You need a quick fix to legacy code
Never start a new provider with SDK v2 in 2026.
Learning Resources
Plugin Framework
SDK v2 (Legacy)
Key Takeaways
- Plugin Framework is the modern standard — Use it for all new providers
- SDK v2 is deprecated — Maintenance mode only
- Plugin Framework offers type safety, better performance, and modern features
- SDK v2 providers can gradually migrate to Plugin Framework
- Custom functions are only available in Plugin Framework
- The ecosystem is actively transitioning to Plugin Framework
What’s Next?
Now that you understand the framework choice, let’s dive into provider architecture and how providers communicate with Terraform.
Further Reading
Last updated: June 30, 2026