How to force resource replacement when Computed/Optional attribute removed from config?

I’m developing a resource for the Terraform Provider for AWS using the Plugin Framework.

The resource has a kms_key_id attribute that is “Optional”. When this attribute is not passed in the API call, AWS assigns a default value “AWS_OWNED_KMS_KEY”. This suggests the attribute should be both “Optional” and “Computed”.

Users can pass valid KMS Key IDs and change keys as well. However, a problem arises when a user decides to remove the kms_key_id attribute from the resource config. The AWS API does not allow reverting to AWS_OWNED_KMS_KEY once a valid KMS Key ID has been set.

My current Terraform implementation does nothing in this scenario. It displays “No changes”, which is dangerous. The previous kms_key_id value remains associated with the AWS resource, but it’s gone from the Terraform resource config. This can lead to confusion and errors.

I believe that when kms_key_id, previously set to an explicit value (not the default AWS_OWNED_KMS_KEY), is removed from the config, it should trigger a complete resource replacement. The newly created resource would then get the AWS_OWNED_KMS_KEY.

The problem is that I’m unable to code this behavior. I’ve customized the ModifyPlan method to detect when kms_key_id is being removed after previously being set to a valid KMS ID, and I set RequiresReplace to a valid attribute path. However, the replacement doesn’t occur unless some other attribute on the resource is also being changed simultaneously with kms_key_id removal.

To summarize: I don’t know how to force resource replacement when a computed and optional attribute is being removed from the config and that’s the only change in the resource. Any advice would be appreciated.

Thanks, Marcin

Hey there @marcinbelczewski :wave:,

Side note: If this doesn’t help, it’d be useful to see your implementation of ModifyPlan for further debugging.

At a high-level looking at what you’re describing, it’s important to note that Terraform core does filter out any RequireReplaces if there is no update to the resource (it doesn’t have to be a different attribute in particular, but something needs to be planned to be updated).

In your use-case, I’d say what is likely missing is setting a new plan value to go along with the required replace, so the new plan value should be AWS_OWNED_KMS_KEY (the planned value being changed is the user set KMS key).


I wrote a naive attribute plan modifier to showcase that idea (although ModifyPlan would also work if you’d prefer that):

func (m examplePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
	// No config value and no planned value, default to AWS_OWNED_KMS_KEY
	if req.ConfigValue.IsNull() && req.PlanValue.IsUnknown() {
		resp.PlanValue = types.StringValue("AWS_OWNED_KMS_KEY")
		return
	}

	// No config value and the state value is not our computed default value ("AWS_OWNED_KMS_KEY"),
	// so we know that we need to replace the resource and set the new value to "AWS_OWNED_KMS_KEY".
	if req.ConfigValue.IsNull() && !req.StateValue.IsNull() && req.StateValue.ValueString() != "AWS_OWNED_KMS_KEY" {
		// If this plan value isn't updated, then Terraform core will ignore the RequiresReplace
		resp.PlanValue = types.StringValue("AWS_OWNED_KMS_KEY")
		resp.RequiresReplace = true
	}
}

With that plan modifier and this schema:

func (r *thingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"kms_key_id": schema.StringAttribute{
				Optional: true,
				Computed: true,
				PlanModifiers: []planmodifier.String{
					ExamplePlanModifier(),
				},
			},
		},
	}
}

You’d get the following config/apply output:

resource "examplecloud_thing" "test" {
  kms_key_id = "1234abcd-12ab-34cd-56ef-1234567890ab"
}
$ terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # examplecloud_thing.test will be created
  + resource "examplecloud_thing" "test" {
      + kms_key_id = "1234abcd-12ab-34cd-56ef-1234567890ab"
    }

Plan: 1 to add, 0 to change, 0 to destroy.
examplecloud_thing.test: Creating...
examplecloud_thing.test: Creation complete after 0s

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
resource "examplecloud_thing" "test" {
  # removed the kms_key_id
}
 $ terraform apply -auto-approve
examplecloud_thing.test: Refreshing state...

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # examplecloud_thing.test must be replaced
-/+ resource "examplecloud_thing" "test" {
      ~ kms_key_id = "1234abcd-12ab-34cd-56ef-1234567890ab" -> "AWS_OWNED_KMS_KEY" # forces replacement
    }

Plan: 1 to add, 0 to change, 1 to destroy.
examplecloud_thing.test: Destroying...
examplecloud_thing.test: Destruction complete after 0s
examplecloud_thing.test: Creating...
examplecloud_thing.test: Creation complete after 0s

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

You can tweak the plan modification for your specific use-case/business rules, but the general idea is when requiring a replacement, change the plan value for the attribute as well if it’s not already.

1 Like

If you’re ever in a situation where you don’t know what the plan value but you need to replace the resource, you can also set the plan value to unknown (as long as the config value is not set, per Terraform’s data consistency rules)

1 Like