Computed optional nested attribute with defaults leads to unstable plan

Hi all,

When using a NestedAttribute (both with ListNestedAttribute and SingleNestedAttribute) that is Optional and Computed with a nested attribute that is also Optional and Computed and that has a Default, the plan becomes unstable. Even if nothing changes, not the config, the state nor the external resource, Terraform keeps reporting an in-place update. I’ve created a very simple provider to demonstrate (with testcase) this issue at GitHub - papegaaij/terraform-provider-jsonfile. Note that this example uses the current time, which changes on every read, but using any value (even a constant) other than the default produces the same effect.

The schema is defined as:

resp.Schema = schema.Schema{
	Attributes: map[string]schema.Attribute{
		"value": schema.StringAttribute{
			Required: true,
		},
		"nested": schema.SingleNestedAttribute{
			Optional: true,
			Computed: true,
			Attributes: map[string]schema.Attribute{
				"time": schema.StringAttribute{
					Computed: true,
					Optional: true,
					Default:  stringdefault.StaticString("1970-01-01 00:00:00"),
				},
			},
		},
	},
}

If you then use the following resource configuration and apply it twice:

resource "jsonfile_data" "data" {
  value = "test"
}

Terraform reports the following plan:

  # jsonfile_data.data will be updated in-place
  ~ resource "jsonfile_data" "data" {
      ~ nested = {
          ~ time = "2024-01-25 10:02:40.552701888 +0100 CET m=+0.017288668" -> (known after apply)
        } -> (known after apply)
        # (1 unchanged attribute hidden)
    }

Am I doing something wrong or is this a bug in Terraform or the plugin framework?

Best regards,
Emond Papegaaij

Hi @papegaaij :wave:

Sorry you ran into trouble here.

The reason you’re seeing the planned changes is because the nested attribute is marked as optional and computed, and when there is no configuration supplied for nested the attribute is marked as unknown. Terraform does not know the value of this attribute until terraform apply is run, hence the in-place updated generated by terraform plan.

One way to tackle this issue would be to modify the schema to include a UseStateForUnknown plan modifier on the nested attribute, if the nested.time attribute is set once and remains unchanged:

	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"value": schema.StringAttribute{
				Required: true,
			},
			"nested": schema.SingleNestedAttribute{
				Optional: true,
				Computed: true,
				PlanModifiers: []planmodifier.Object{
					objectplanmodifier.UseStateForUnknown(),
				},
				Attributes: map[string]schema.Attribute{
					"time": schema.StringAttribute{
						Computed: true,
						Optional: true,
						Default:  stringdefault.StaticString("1970-01-01 00:00:00"),
					},
				},
			},
		},
	}

There are some further docs regarding UseStateForUnknown which go into a little more detail.

It’s perhaps worth mentioning that the provider you created GitHub - papegaaij/terraform-provider-jsonfile (thanks for providing this) mutates the state in the Read method by changing the value attribute, resulting in planned changes the first time Read is called.

Hi @bendbennett ,

Thanks for the quick response.

I do know about UseStateIfUnknown, and it does indeed prevent the in-place update. However, the documentation of UseStateIfUnknown states:

This is useful for reducing (known after apply) plan outputs for computed attributes which are known to not change over time.

As you noticed, time does change over time. I must admit, this is a bit of constructed example, but our API has many attributes that are computed, and may and will change over time. These do cause a lot of trouble with unstable plans. Do you recommend puting UseStateIfUnknown on all computed attributes?

One thing I do not understand is why an in-place update is needed to refresh a computed attribute. As far as I understand it, in-place update refers to updating the external resource, not just refreshing the state. Over the past few weeks, I’ve seen many plans with only changes like attr = "value" -> (known after apply), just like the one I shared in my first post. These plans do not contain any changed known values. Why does Terraform want to perform an in-place update on the external resource, just to get some fresh values for computed attributes? This only seems to happen on nested attributes, not on attributes in the root of the resource. For us, these in-place updates are really problematic, because in many cases they are not supported by our API.

Best regards,
Emond

What Terraform “wants” here is only to follow the plan returned from the provider. If the provider returns a plan that says something is unknown, then Terraform must apply that plan to resolve the unknown values. The only way to avoid changes in the plan is to return an unchanged value from PlanResourceChange.

An easy way out here is to only have the attributes get updated during ReadResource, and let the plan use the prior state value (UseStateIfUnknown in framework terms) to avoid further changes.

A more complex approach would be to customize the logic such that the unknown attributes stay unknown only if there are other changes which warrant applying new values. This ensures the computed values are updated again at the same time as apply if that matters to the logic of the resource.

These types of attribute however are always going to cause some noise in the system, because Terraform does not expect resources to change outside of Terraform’s view. This noise will generally be detected as what is often called “drift”, but can cause surprises when the values are captured in other managed resources’ state somehow, and start causing other changes on every plan.

@papegaaij I’m not sure if this will help clarify the sequence of events a little, but descriptions of the order of RPCs that occur during terraform plan and terraform apply are available in RPCs and Framework Functionality. This provides an illustration as to why James’ suggestion of modifying the state values for computed attributes in the Read method (the ReadResource RPC) and setting the UseStateForUnknown plan modifier on the attribute will result in updating of the state values for these attributes without any “updated in-place” notifications.

Thanks for your help here @jbardin and @bendbennett.

I did some more testing, inspection and reading, and I think my understanding of this all is a bit better now. Somehow, I got the idea that the State used by UseStateIfUnknown was the state prior to Read, but I see now that Read is called first, and the state it produces is fed to the PlanModifier. I think I got confused by the use of the term ‘prior state’. It seems using UseStateIfUnknown does cause the state to be updated, even if no changes are applied.

It makes total sense that using an unstable attribute in dependent resources will cause unstable plans. That’s fine. Some of these attributes do not change very often, and others are more of an informational nature and could be used for output for example.

One thing I do not yet understand is when to use UseStateIfUnknown and when not to use it, especially on computed attributes. The documentation states the following:

Use this when it is known that an unconfigured value will remain the same after a resource update.

This is indeed where I do see some issues now. In my example, the time changes on every call (both Read and Update). A change to the configuration or external resource, will trigger an in-place update (as expected). However, the time attribute changes in Update, and this causes Terraform to report Provider produced inconsistent result after apply. The time example is rather extreme, but something similar might happen in our case: there is a small chance an attribute’s value may change between Read and Update.

Best regards,
Emond

Something to keep in mind is that Terraform must ensure that the plan is executed during apply exactly as it recorded (which results in Provider produced inconsistent result after apply when it that contract is violated). There’s no leeway here, the resource is expected to do what is necessary to fill in unknown values, and nothing more.

This means that if you returned a changed attribute during the plan (a new timestamp for example), you must apply that exact same attribute value during apply in order for Terraform to ensure the integrity of all other resource plans. If you might need to return a different value during apply, then you must mark it as unknown during the plan. When you have an attribute that continually changes like a timestamp, you cannot just re-read that attribute again during apply (which is a common pattern in providers to easily fill-in all attributes after an update), it will require special handling to ensure you maintain the planned value in the response.