Terraform
Plan Modification
After validation and before applying configuration changes, Terraform generates a plan that describes the expected values and behaviors of those changes. Resources can then tailor the plan to match the expected end state, prevent errant in-place updates, or return any diagnostics.
Terraform and the framework support multiple types of plan modification on resources:
- Adjusting unknown attribute values, such as providing a known remote default value when a configuration is not present.
- Marking resources that should be replaced, such as when an in-place update is not supported for a change.
- Returning warning or error diagnostics on planned resource creation, update, or deletion.
Plan modification can be added on resource schema attributes or an entire resource. Use resource-based plan modification if access to the configured resource is necessary.
Plan Modification Process
When the provider receives a request to generate the plan for a resource change via the framework, the following occurs:
- Set any attributes with a null configuration value to the default value.
- If the plan differs from the current resource state, the framework marks computed attributes that are null in the configuration as unknown in the plan. This is intended to prevent unexpected Terraform errors. Providers can later enter any values that may be known.
- Run attribute plan modifiers.
- Run resource plan modifiers.
When the Resource
interface Update
method runs to apply a change, all attribute state values must match their associated planned values or Terraform will generate a Provider produced inconsistent result
error. You can mark values as unknown in the plan if the full expected value is not known.
Refer to the Resource Instance Change Lifecycle document for more details about the concepts and processes relevant to the plan and apply workflows.
During the terraform plan
and terraform apply
commands, Terraform calls the provider PlanResourceChange
RPC, in which the framework calls the resource.Resource
interface Schema
method attribute plan modifiers and the ModifyPlan
method on resources that implement the resource.ResourceWithModifyPlan
interface.
Attribute Plan Modification
You can supply the attribute type PlanModifiers
field with a list of plan modifiers for that attribute. For example:
// Typically within the schema.Schema returned by Schema() for a resource.
schema.StringAttribute{
// ... other Attribute configuration ...
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
}
If defined, plan modifiers are applied to the current attribute. If any nested attributes define plan modifiers, then those are applied afterwards. Any plan modifiers that return an error will prevent Terraform from applying further modifiers of that attribute as well as any nested attribute plan modifiers.
Common Use Case Attribute Plan Modifiers
The framework implements some common use case modifiers in the typed packages under resource/schema/
, such as resource/schema/stringplanmodifier
:
RequiresReplace()
: If the value of the attribute changes, in-place update is not possible and instead the resource should be replaced for the change to occur. Refer to the Go documentation for full details on its behavior.RequiresReplaceIf()
: Similar toresource.RequiresReplace()
, however it also accepts provider-defined conditional logic. Refer to the Go documentation for full details on its behavior.RequiresReplaceIfConfigured()
: Similar toresource.RequiresReplace()
, however it also will only trigger if the practitioner has configured a value. Refer to the Go documentation for full details on its behavior.UseStateForUnknown()
: Copies the prior state value, if not null. This is useful for reducing(known after apply)
plan outputs for computed attributes which are known to not change over time.
Creating Attribute Plan Modifiers
To create an attribute plan modifier, you must implement the one of the planmodifier
package interfaces. For example:
// useStateForUnknownModifier implements the plan modifier.
type useStateForUnknownModifier struct{}
// Description returns a human-readable description of the plan modifier.
func (m useStateForUnknownModifier) Description(_ context.Context) string {
return "Once set, the value of this attribute in state will not change."
}
// MarkdownDescription returns a markdown description of the plan modifier.
func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) string {
return "Once set, the value of this attribute in state will not change."
}
// PlanModifyBool implements the plan modification logic.
func (m useStateForUnknownModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
// Do nothing if there is no state value.
if req.StateValue.IsNull() {
return
}
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}
resp.PlanValue = req.StateValue
}
Optionally, you may also want to create a helper function to instantiate the plan modifier. For example:
// UseStateForUnknown returns a plan modifier that copies a known prior state
// value into the planned value. Use this when it is known that an unconfigured
// value will remain the same after a resource update.
//
// To prevent Terraform errors, the framework automatically sets unconfigured
// and Computed attributes to an unknown value "(known after apply)" on update.
// Using this plan modifier will instead display the prior state value in the
// plan, unless a prior plan modifier adjusts the value.
func UseStateForUnknown() planmodifier.Bool {
return useStateForUnknownModifier{}
}
Caveats
Terraform Data Consistency Rules
Terraform core implements data consistency rules between configuration, plan, and state data. For example, if an attribute value is configured, it is never valid to change that value in the plan except being set to null on resource destroy. The framework does not raise its own targeted errors in many situations, so it is the responsibility of the developer to account for these rules when implementing plan modification logic.
Prior State Under Lists and Sets
Attribute plan modifiers under the following must take special consideration if they rely on prior state data:
- List nested attributes
- List nested blocks
- Set nested attributes
- Set nested blocks
These data structures are implemented based on array indexing, which the framework always sends the exact representation given across the protocol. If list/set elements are rearranged or removed, Terraform nor the framework performs any re-alignment of prior state for those elements.
In this example, potentially unexpected prior state may be given to attribute plan modifier request:
- A list nested attribute with two elements in configuration is saved into state
- The configuration for the first element is removed
- The list nested attribute with now one element still receives the prior state of the first element
Checking Resource Change Operations
Plan modifiers execute on all resource change operations: creation, update, and destroy. If the plan modification logic is sensitive to these details, check the request data to determine the current operation.
Implement the following to check whether the resource is being created:
func (m ExampleModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
// Check if the resource is being created.
if req.State.Raw.IsNull() {
// ...
}
// ...
}
Implement the following to check whether the resource is being destroyed:
func (m ExampleModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
// Check if the resource is being destroyed.
if req.Plan.Raw.IsNull() {
// ...
}
// ...
}
Resource Plan Modification
Resources also support plan modification across all attributes. This is helpful when working with logic that applies to the resource as a whole, or in Terraform 1.3 and later, to return diagnostics during resource destruction. Implement the resource.ResourceWithModifyPlan
interface to support resource-level plan modification. For example:
// Ensure the Resource satisfies the resource.ResourceWithModifyPlan interface.
// Other methods to implement the resource.Resource interface are omitted for brevity
var _ resource.ResourceWithModifyPlan = ThingResource{}
type ThingResource struct {}
func (r ThingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
// Fill in logic.
}
Resource Destroy Plan Diagnostics
Support for handling resource destruction during planning is available in Terraform 1.3 and later.
Implement the ModifyPlan
method by checking if the resource.ModifyPlanRequest
type Plan
field is a null
value:
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() {
// Return an example warning diagnostic to practitioners.
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.",
)
}
}
Ensure the response plan remains entirely null
when the request plan is entirely null
.