Terraform
Framework and SDKv2 Feature Comparison
We recommend using the plugin framework to develop your provider because it offers significant benefits in comparison to SDKv2. We designed the framework with feedback from thousands of existing providers, so the framework significantly improves upon the functionality available in SDKv2.
This page is a continuation of the Framework Benefits page, which describes the higher level coding improvements over SDKv2. The following features are only available in the framework.
Expanded Access to Configuration, Plan, and State Data
Providers receive up to three sources of schema-based data during Terraform operation requests: configuration, plan, and prior state. The SDKv2 combines this data into a single schema.ResourceData
type, which you implement differently depending on the operation. Certain ResourceData
methods are only valid during certain operations and trying to get data from an explicit source is problematic in many cases.
In the following SDKv2 example, the code comments highlight issues with the single data type:
func ThingResourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
d.Get("...") // plan unless unknown; no explicit access to configuration
d.GetChange("...") // extraneous old value, use d.Get() instead
d.HasChange("...") // always true, no prior state
d.Set("...") // saved into new state
}
func ThingResourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
d.Get("...") // prior state
d.GetChange("...") // no changes as only prior state is available
d.HasChange("...") // always false
d.Set("...") // saved into new state
}
func ThingResourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
d.Get("...") // plan unless unknown; no explicit access to configuration or prior state
d.GetChange("...") // prior state and plan unless unknown
d.HasChange("...") // comparison of prior state and plan
d.Set("...") // saved into new state
}
func ThingResourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
d.Get("...") // prior state
d.GetChange("...") // no changes as only prior state is available
d.HasChange("...") // always false
d.Set("...") // extraneous, resource destroy leaves no state
}
The framework alleviates these issues by exposing configuration, plan, and state data as separate attributes on request and response types that only expose the data available to the given operation.
In the following framework example, the code comments show the available data that matches each operation.
func (r ThingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
req.Config // configuration data
req.Plan // plan data
// No req.State as it is always null
// No resp.Config as configuration cannot be set by provider during creation
// No resp.Plan as plan cannot be set by provider during creation
resp.State // new state data to save
}
func (r ThingResource) Read(ctx 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 data
// No resp.Config as configuration cannot be set by provider during read
// No resp.Plan as there is no plan during read
resp.State // new state data to save
}
func (r ThingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
req.Config // configuration data
req.Plan // plan data
req.State // prior state data
// No resp.Config as configuration cannot be set by provider during update
// No resp.Plan as plan cannot be set by provider during update
resp.State // new state data to save
}
func (r ThingResource) Delete(ctx 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 data
// No resp.Config as configuration cannot be set by provider during delete
// No resp.Plan as it cannot be adjusted
resp.State // only available to explicitly remove on error
}
Schema Data Models
In the SDKv2, you must fetch configuration, plan, and state data separately for each attribute or type. In the framework, you can fetch all of the configuration, plan, and state data at once. This approach lets you declare a single data model for a schema, which guarantees correctness and consistency across operations.
In the following SDKv2 example, you must fetch the data for each attribute unless you save the schema as a variable and reference it in the operation logic.
attribute1 := d.Get("attribute1") // any type
attribute2 := d.Get("attribute2") // any type
attribute3 := d.Get("attribute3") // any type
Some SDKv2 providers opted to type assert during these calls, which had the potential to cause Go runtime panics if they did not also check the assertion boolean.
// Example showing panic-safe SDK data handling
attribute1, ok := d.Get("attribute1").(bool) // assuming schema.TypeBool
if !ok {
// provider-defined error handling
}
attribute2, ok := d.Get("attribute2").(int) // assuming schema.TypeInt
if !ok {
// provider-defined error handling
}
attribute3, ok := d.Get("attribute3").(string) // assuming schema.TypeString
if !ok {
// provider-defined error handling
}
The Fully Exposed Value States section goes into other issues and quirks with attempting to handle SDKv2 data.
Data with the framework can be modeled as a custom type and the operation of getting or setting the data will return framework-defined errors, if necessary.
In the following framework example, a provider-defined type receives all schema-based data.
// Example schema data model type
type ThingResourceModel struct {
Attribute1 types.Bool `tfsdk:"attribute1"` // assuming types.BoolType attribute
Attribute2 types.Int64 `tfsdk:"attribute2"` // assuming types.Int64Type attribute
Attribute3 types.String `tfsdk:"attribute3"` // assuming types.StringType attribute
}
// In resource logic
var data ThingResourceModel
diags := req.Plan.Get(ctx, &data) // framework-defined errors
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
With Required
attributes, you can replace the framework types in the schema data model with standard Go types (e.g. bool
) to further simplify data handling, if desired.
Fully Exposed Value States
Terraform supports three states for any value: null (missing), unknown ("known after apply"), and known. The SDKv2 does not expose or fully support null and unknown value states to providers. Instead, the Get()
method on these value states returns Go type zero-values such as ""
for schema.TypeString
, 0
for schema.TypeInt
, and false
for schema.TypeBool
. Other methods, such as GetOk()
and GetOkExists()
, have slightly different functionality for each type and operation, especially for collection types.
In the following SDKv2 example, the code comments explain issues with the single data type.
// Assuming a schema of:
//
// "string_attribute": &schema.Schema{
// Computed: true,
// Optional: true,
// Type: schema.TypeString,
// }
//
// and a configuration that does not set the value (null state).
//
// resource “examplecloud_thing” “example” {
// # no string_attribute = “...”
// }
func ThingResourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
d.Get("string_attribute") // ""
d.GetOk("string_attribute") // may return true depending on prior state
d.GetOkExists("string_attribute") // may return true depending on prior state
}
The framework type system fully exposes null, unknown, and known value states. You can reliably query each value with the IsNull()
or IsUnknown()
methods.
In the following framework example, you can determine the correct value state.
// Assuming a schema of:
//
// "string_attribute": schema.StringAttribute{
// Computed: true,
// Optional: true,
// }
//
// and a configuration that does not set the value (null state).
//
// resource “examplecloud_thing” “example” {
// # no string_attribute = “...”
// }
func (r ThingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var config, plan types.String
req.Config.GetAttribute(ctx, path.Root("string_attribute"), &config)
req.Plan.GetAttribute(ctx, path.Root("string_attribute"), &plan)
config.IsNull() // true
config.IsUnknown() // false
config.ValueString() // ""
plan.IsNull() // true
plan.IsUnknown() // false
plan.ValueString() // ""
}
Unrestricted Type System
The framework type system exposes the majority of Terraform types and values. It is also extensible because it lets you define new types that are specific to your provider.
Custom Attribute Types
You can implement custom types for your provider that expose data with convenient Go types, methods, and built-in validation.
The following framework example uses a custom timetypes.RFC3339Type
attribute type instead of types.StringType
. The timetypes.RFC3339Type
attribute type is associated with a timetypes.RFC3339
value type. The attribute type automatically validates whether the string can be parsed as an RFC3339 timestamp and the value type exposes a Time() time.Time
method for easier usage over a regular string value.
"rfc3339": schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Required: true,
},
The following framework example uses the custom timetypes.RFC3339
value type to expose the time.Time
value.
// Example schema data model
type ThingResourceModel struct{
RFC3339 timetypes.RFC3339 `tfsdk:"rfc3339"`
}
// In resource logic, omitting diagnostics handling for brevity
var data ThingResourceModel
req.Plan.Get(ctx, &data)
data.RFC3339.Time() // time.Time
Complex Map Types
The framework type system does not have any restrictions for using complex types as the value for a map type. SDKv2 restricted map values to string, number, and boolean types.
This framework example declares a map type with a list of string values.
schema.MapAttribute{
// ... other fields ...
ElementType: types.ListType{
ElemType: types.StringType,
},
}
If you need to declare additional schema behaviors for the map values, you can use map nesting mode in Protocol Version 6 Nested Attributes, which is also only available in the framework.
Object Type
The framework type system supports the Terraform object type, which you can use to declare attribute name to value mappings without additional schema behaviors. These differ from maps by requiring specific names and their values to always exist. SDKv2 did not directly expose this type.
The following framework example declares an object type with two attributes.
schema.ObjectAttribute{
// ... other fields ...
AttributeTypes: map[string]attr.Type{
"bool_attribute": types.BoolType,
"string_attribute": types.StringType,
},
}
If you need to declare additional schema behaviors for the object values, you can use the single nesting mode in Protocol Version 6 Nested Attributes, which is also only available in the framework.
Protocol Version 6 Nested Attributes
Protocol version 6 is the latest version of the protocol between Terraform and providers. Only the framework supports version 6.
Version 6 lets you declare schemas with nested attributes in addition to blocks. Nested attributes support schema behaviors and practitioners using your provider can configure them with expressions instead of with dynamic blocks. Nested attributes support includes four nesting modes:
- List: Ordered collection of nested attributes
- Map: Collection of string keys to nested attributes.
- Set: Unordered collection of nested attributes.
- Single: Single object of nested attributes that is useful for replacing list blocks with a single element.
In the following configuration example, a schema uses a list block that is difficult to dynamically configure.
locals {
calls = toset([
{call_me: “example1”, maybe: true},
{call_me: “example2”, maybe: false},
])
}
resource “examplecloud_thing” “example” {
dynamic “list_block” {
for_each = local.calls
content {
call_me = list_block.value.call_me
maybe = list_block.value.maybe
}
}
}
In the following configuration example, a schema uses list nested attributes to simplify the configuration.
locals {
calls = [
{call_me: “example1”, maybe: true},
{call_me: “example2”, maybe: false},
]
}
resource “examplecloud_thing” “example” {
list_nested_attributes = local.calls # or a for expression, etc.
}
The following framework example shows the schema definition for the list nested attributes.
schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
“call_me”: schema.StringAttribute{
Required: true,
},
“maybe”: schema.BoolAttribute{
Optional: true,
Sensitive: true,
},
},
},
// ... other fields ...
}
Unrestricted Validation Capabilities
The framework exposes many more configuration validation integration points than SDKv2. It is also extensible with provider-defined types that implement validation in the type itself.
Collection Type Validation
Attribute validation in the framework is not restricted by type.
This framework example validates all list values against a set of acceptable values.
schema.ListAttribute{
// ... other fields ...
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.StringValuesAre(
stringvalidator.OneOf("one", "two", "three"),
),
},
}
This framework example checks whether map keys are between 3 and 50 characters in length using validators available in terraform-plugin-framework-validators
.
schema.MapAttribute{
// ... other fields ...
ElementType: types.StringType,
Validators: []validator.Map{
mapvalidator.KeysAre(
stringvalidator.LengthBetween(3, 50),
),
},
}
Type-Based Validation
Attribute validation supports attribute types that declare their own validation, in addition to any validators on the attribute itself.
In the following framework example, the custom type ensures that the string is a valid RFC3339 string, and the attribute can declare additional validation.
schema.StringAttribute{
// ... other fields ...
CustomType: timetypes.RFC3339Type{}, // automatically validates string is RFC3339
Validators: []validator.String{
// additional validation, if desired
},
}
Declarative Schema Validation
The framework supports schema-level validation with reusable and declarative validators. In certain cases, such as when you would use SDKv2 AtLeastOneOf
, this approach can reduce overlapping validation errors and make logic easier to understand.
In the following SDKv2 example, multiple attributes can raise multiple errors.
map[string]*schema.Schema{
“attribute_one”: {
AtLeastOneOf: []string{“attribute_two”}, // does this need attribute_one?
// ... other fields ...
},
“attribute_two”: {
AtLeastOneOf: []string{“attribute_one”}, // is this necessary?
// ... other fields ...
},
}
In the following framework example, the validation logic raises a single error when the resource configuration does not include at least one of the specified attributes.
func (r ThingResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{
resourcevalidator.AtLeastOneOf(
path.MatchRoot("attribute_one"),
path.MatchRoot("attribute_two"),
),
}
}
Imperative Schema Validation
The framework supports schema-level validation with custom logic in addition to the declarative validators. This support lets you fully customize the validation to implement complex validation logic.
In the following framework example, the resource implements custom validation logic.
func (r ThingResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
// custom logic
}
Path Expressions
The framework includes a schema path implementation that lets you target attributes of any type or nesting level. This feature lets you build paths without knowing the special string syntax of the SDKv2, instead using Go ecosystem features such as suggestions from editor integrations.
In the following framework example, the path is absolute to the first element of a list.
path.Root("list_attribute").AtListIndex(0)
Additionally, the framework supports expressions on top of these paths, which enables logic such as matching all indices in a list, relative paths, and parent paths.
The following framework example validates whether the two attributes within the same list element conflict with each other.
schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
“attribute_one”: schema.StringAttribute{
Validators: []validator.String{
stringvalidator.ConflictsWith(
path.MatchRelative().AtParent().AtName(“attribute_two”),
),
},
// ... other fields ...
},
“attribute_two”: { /* … */ },
},
},
// ... other fields ...
}
Import Warning Diagnostics
The framework supports diagnostics through all Terraform operations. The SDKv2 does not support diagnostics with some operations, such as import.
The following framework example returns a warning to practitioners when they import the resource.
func (r ThingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resp.Diagnostics.AddWarning(
“Resource Import Considerations”,
“The API does return the password attribute, which will show as a plan ”+
“difference in Terraform unless the lifecycle configuration block “+
“ignore_changes argument includes password.”
)
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
Destroy Plan Diagnostics
With the framework, Terraform version 1.3 and later supports calling the provider when Terraform is planning to destroy a resource. SDKv2 does not support this functionality.
In this framework example, the resource will raise a warning when planned for destruction to give practitioner more information:
func (r ThingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
// If the entire plan is null, the resource is planned for destruction.
if req.Plan.Raw.IsNull() {
resp.Diagnostics.AddWarning(
"Resource Destruction Considerations",
"Applying this resource destruction will only remove the resource from the Terraform state "+
"and will not call the deletion API due to API limitations. Manually use the web "+
"interface to fully destroy this resource.",
)
}
}
Resource Private State Management
Each provider can maintain resource private state data in Terraform state. Terraform never accesses resource private state or includes the information in plans, but providers can use this private data for advanced use cases. For example, a provider could use resource private state to store API ETag values that are not beneficial for practitioners. SDKv2 does not support this functionality.
Refer to the Manage Private State documentation for more information.