Terraform
CRUD functions
In Terraform, a resource represents a single instance of a given resource type. They modify a specific resource in the API and in Terraform's state through a set of Create, Read, Update, and Delete (CRUD) functions. A resource's CRUD functions implement the logic required to manage your resources with Terraform. Refer to Resources - Define Resources in the Framework documentation for details.
This page explains how to migrate a resource's CRUD functions from SDKv2 to the plugin Framework.
SDKv2
In SDKv2, a resource's CRUD functions are defined by populating the relevant fields (e.g., CreateContext
,
ReadContext
) on the schema.Resource
struct.
The following code shows a basic implementation of CRUD functions with SDKv2.
func resourceExample() *schema.Resource {
return &schema.Resource{
CreateContext: create,
ReadContext: read,
UpdateContext: update,
DeleteContext: delete,
/* ... */
Framework
In the Framework, you implement CRUD functions for your resource by defining a type that implements the
resource.Resource
interface. To define functions related to state upgrade, import, and plan modification,
implement their respective interfaces on your resource: ResourceWithUpgradeState
, ResourceWithImportState
, and
ResourceWithModifyPlan
.
The following code shows how you define a resource.Resource
which implements CRUD functions with the Framework.
type resourceExample struct {
p provider
}
func (r *resourceExample) Create(ctx context.Context, req resource.CreateRequest, resp
*resource.CreateResponse) {
/* ... */
}
func (r *resourceExample) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
/* ... */
}
func (r *resourceExample) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
/* ... */
}
func (r *resourceExample) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
/* ... */
}
Migration Notes
Remember the following differences between SDKv2 and the Framework when completing the migration.
- In SDKv2 it is not necessary to define functions for parts of the CRUD lifecycle that are not used by a given
resource. For instance, if the resource does not support in-place modification, you do not need to define an
Update
function. In the Framework, you must implement each of the CRUD lifecycle functions on all resources to satisfy theResource
interface, even if the function does nothing. - In SDKv2, the
Update
function (even if empty or missing) would automatically copy the request plan to the response state. This could be problematic when theUpdate
function also returned errors as additional steps were required to disable the automatic SDKv2 behavior. In the Framework, theUpdate
method must be written to explicitly copy data fromreq.Plan
toresp.State
to actually update the resource's state and preventProvider produced inconsistent result after apply
errors from Terraform. - In SDKv2, calling
d.SetId("")
would signal resource removal. In the Framework, this is replaced withresp.State.RemoveResource()
. Resource removal should only occur during theRead
method to preventProvider produced inconsistent result after apply
errors from Terraform during other operations. TheDelete
method will automatically callresp.State.RemoveResource()
if there are no errors. - In SDKv2, you get and set attribute values in Terraform's state by calling
Get()
andSet()
onschema.ResourceData
. In the Framework, you get attribute values from the configuration and plan by accessingConfig
andPlan
onresource.CreateRequest
. You set attribute values in Terraform's state by mutatingState
onresource.CreateResponse
. - In SDKv2, certain resource schema definition and data consistency errors are only visible as Terraform warning logs by default. After migration, these errors will always be visible to practitioners and prevent further Terraform operations. The SDKv2 resource data consistency errors documentation discusses how to find these errors in SDKv2 resources and potential solutions prior to migrating. See the Resolving Data Consistency Errors section for Plugin Framework solutions during migration.
Example
SDKv2
The following example from shows implementations of CRUD functions on the with SDKv2.
The UpdateContext
function is not implemented because the provider does not support updating this resource.
func resourceExample() *schema.Resource {
return &schema.Resource{
CreateContext: create,
ReadContext: readNil,
DeleteContext: RemoveResourceFromState,
/* ... */
The following example shows the implementation of the create()
function with SDKv2. The implementations of
the readNil()
and RemoveResourceFromState()
functions are not shown for brevity.
func create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if err := d.Set("example_attribute", "value"); err != nil {
diags = append(diags, diag.Errorf("err: %s", err)...)
return diags
}
return nil
}
Framework
The following shows the same section of provider code after the migration.
This code implements the Create
function for the example_resource
resource with the Framework.
func (r *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan exampleModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
plan.ExampleAttribute = types.StringValue("value")
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
Resolving Data Consistency Errors
Note
See the SDKv2 data consistency errors documentation for background info, debugging tips, and potential SDKv2 solutions.
Planned Value does not match Config Value
If an SDKv2 resource is raising this type of error or warning log:
TIMESTAMP [WARN] Provider "TYPE" produced an invalid plan for ADDRESS, but we are tolerating it because it is using the legacy plugin SDK.
The following problems may be the cause of any confusing errors from downstream operations:
- .ATTRIBUTE: planned value cty.StringVal("VALUE") does not match config value cty.StringVal("value")
This occurs for attribute schema definitions that are Optional: true
and Computed: true
; where the planned value, returned by the provider, does not match the attribute's config value or prior state value. For example, value's for an attribute of type string must match byte-for-byte.
An example root cause of this issue could be from API normalization, such as a JSON string being returned from an API and stored in state with differing whitespace then what was originally in config.
SDKv2 Example
Here is an example of an SDKv2 resource schema and terraform config that simulates this data consistency error:
func thingResource() *schema.Resource {
return &schema.Resource{
// ...
Schema: map[string]*schema.Schema{
"word": {
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: func(word interface{}) string {
// This simulates an API returning the 'word' attribute as all uppercase,
// which is stored to state even if it doesn't match the config or prior value.
return strings.ToUpper(word.(string))
},
},
},
}
}
resource "examplecloud_thing" "this" {
word = "value"
}
A warning log will be produced and the resulting state after applying a new resource will be VALUE
instead of value
.
Migrating to Plugin Framework
When a resource with this behavior and prior state is migrated to Plugin Framework, depending on the business logic, you could potentially see:
- Resource drift in the plan; Terraform will always detect a change between the config and state value. If no modification is implemented, you could see drift in the plan:
resource "examplecloud_thing" "this" {
word = "value"
}
examplecloud_thing.this: Refreshing state...
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# examplecloud_thing.this will be updated in-place
~ resource "examplecloud_thing" "this" {
~ word = "VALUE" -> "value"
}
Plan: 0 to add, 1 to change, 0 to destroy.
- If you mimic the original SDKv2 behavior of storing a different value from config/prior value into state in the
Update
method, you will see an error like below:
examplecloud_thing.this: Modifying...
╷
│ Error: Provider produced inconsistent result after apply
│
│ When applying changes to examplecloud_thing.this, provider "provider[\"TYPE\"]" produced an unexpected
│ new value: .word: was cty.StringVal("value"), but now cty.StringVal("VALUE").
│
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
Recommended Solution
To solve this issue, the provider code must preserve the config value or prior state value when producing the new state. The recommended way to implement this logic is by creating a custom type with semantic equality logic. A custom type can be shared across multiple resource attributes and will ensure that the semantic equality logic is invoked during the Read
, Create
, and Update
methods respectively.
For the above example, the semantic equality implementation below would resolve the resource drift and error:
Tip
The example code below is a partial implementation of a custom type, please see the Custom Value Type documentation for guidance.
type CaseInsensitive struct {
basetypes.StringValue
}
// ... custom value type implementation
// StringSemanticEquals returns true if the given string value is semantically equal to the current string value. (case-insensitive)
func (v CaseInsensitive) StringSemanticEquals(_ context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
newValue, ok := newValuable.(CaseInsensitive)
if !ok {
diags.AddError(
"Semantic Equality Check Error",
"An unexpected value type was received while performing semantic equality checks. "+
"Please report this to the provider developers.\n\n"+
"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+
"Got Value Type: "+fmt.Sprintf("%T", newValuable),
)
return false, diags
}
return strings.EqualFold(newValue.ValueString(), v.ValueString()), diags
}