---
page_title: 'Resources - CRUD Functions: Migrating from SDKv2 to the Framework'
description: >-
  Migrate resource create, read, update, and delete (CRUD) functions from SDKv2 to the plugin Framework.
---

# 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](/terraform/plugin/framework/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.

```go
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.

```go
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 the
`Resource` 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 the `Update` function also returned errors as additional steps were required to disable the automatic SDKv2 behavior. In the Framework, the `Update` method must be written to explicitly copy data from `req.Plan` to `resp.State` to actually update the resource's state and prevent `Provider produced inconsistent result after apply` errors from Terraform.
- In SDKv2, calling `d.SetId("")` would signal resource removal. In the Framework, this is replaced with `resp.State.RemoveResource()`. Resource removal should only occur during the `Read` method to prevent `Provider produced inconsistent result after apply` errors from Terraform during other operations. The `Delete` method will automatically call `resp.State.RemoveResource()` if there are no errors.
- In SDKv2, you get and set attribute values in Terraform's state by calling `Get()` and `Set()` on
`schema.ResourceData`. In the Framework, you get attribute values from the configuration and plan by accessing
`Config` and `Plan` on `resource.CreateRequest`. You set attribute values in Terraform's state by mutating `State`
on `resource.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](/terraform/plugin/sdkv2/resources/data-consistency-errors) discusses how to find these errors in SDKv2 resources and potential solutions **prior** to migrating. See the [Resolving Data Consistency Errors](#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.

```go
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.

```go
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.

```go
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](/terraform/plugin/sdkv2/resources/data-consistency-errors) for background info, debugging tips, and potential SDKv2 solutions.

</Note>

### Planned Value does not match Config Value

If an SDKv2 resource is raising this type of error or [warning log](/terraform/plugin/sdkv2/resources/data-consistency-errors#checking-for-warning-logs):

```text
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:

```go
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))
				},
			},
		},
	}
}
```

```hcl
resource "examplecloud_thing" "this" {
  word = "value"
}
```

A [warning log](/terraform/plugin/sdkv2/resources/data-consistency-errors#checking-for-warning-logs) 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](/terraform/plugin/framework/resources/plan-modification) is implemented, you could see drift in the plan:
```hcl
resource "examplecloud_thing" "this" {
  word = "value"
}
```
```text
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:
```text
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](/terraform/plugin/framework/handling-data/types/custom) with [semantic equality logic](/terraform/plugin/framework/handling-data/types/custom#semantic-equality). 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](/terraform/plugin/framework/handling-data/types/custom#value-type) for guidance.  

</Tip>

```go
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
}
```

