# forces replacement is Terraform telling you that the attribute on that line cannot be changed on the object that already exists, so the provider has to destroy it and build a new one to give you the value you asked for. Search the plan for that comment and you have the attribute. The symbol beside the resource tells you the ordering: -/+ destroys first and then creates, and +/- creates the replacement first, which you only get where the resource asks for it.
terraform plan, abridged
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:
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ ami = ... -> ... # forces replacement
~ id = ... -> (known after apply)
...
}
Plan: 1 to add, 0 to change, 1 to destroy.
Reading the lines Terraform printed
Three separate lines in that output are each telling you something different, and they are usually read as one.
The legend, at the top of the plan
Terraform prints the symbols it is about to use before it uses them. -/+ destroy and then create replacement is the default ordering. +/- create replacement and then destroy is the inverted one. If the legend only mentions the first, nothing in the plan is being created before it is destroyed.
The header, above the resource
Terraform states why it is replacing this particular object, and there are four sentences it uses. They are not interchangeable and the difference is worth a moment:
What the header says
What caused it
must be replaced
An attribute in your diff cannot be changed in place. The change in front of you caused this.
will be replaced, as requested
Somebody passed -replace for this address. It is not in the diff at all.
is tainted, so must be replaced
The state entry carries a tainted mark, from a failed create or from somebody running terraform taint. Also not in the diff.
will be replaced due to changes in replace_triggered_by
A replace_triggered_by reference in the resource's lifecycle block changed. Something else in the diff did this.
Only the first and the last are consequences of the change being reviewed. The middle two arrived from a command somebody ran, and they will not be explained by anything in the pull request.
The comment, at the end of an attribute line
# forces replacement marks the attribute responsible. More than one attribute can carry it, and every one of them is a reason on its own: reverting one of three does not avoid the replacement.
What the summary line at the foot of the plan does not tell you is the ordering. Plan: 1 to add, 0 to change, 1 to destroy is what a replacement counts as, and it is identical under both symbols. It is also what an unrelated create plus an unrelated destroy counts as, which is why a plan summary is a poor place to decide anything.
Finding the attribute responsible
In a plan you can read, search for the literal string forces replacement. In a plan of several thousand lines, which is when you actually need this, read the machine-readable form instead. Terraform records the same fact as replace_paths on each resource change: a list of attribute paths, and nothing else.
That prints one line per resource being replaced, naming the paths and no values. A nested path comes back as its elements, so an attribute inside a block at index zero reads as delegation.0.name there, where Terraform's own display form is delegation[0].name.
The JSON is also the answer to the case where the human-readable plan is not available to you: a plan file from CI, an artefact attached to a pull request, or a plan somebody has pasted a fragment of. terraform show -json on the saved plan gives the whole thing back.
One caution about the JSON that matters more than it sounds: a plan file holds the before and after values of every attribute in the change, in the clear, whether or not Terraform marked them sensitive. It is not an artefact to leave on a CI runner.
Why an attribute cannot be changed in place
The rule is the provider's, not Terraform's. Every attribute in a provider's schema is either updatable or not, and the ones that are not are the ones where the remote API has no operation to change them on an existing object. There is no negotiation and no flag: if the API cannot do it, the only route to the value you asked for is a new object.
The attributes that usually turn out to be in that group:
Anything that is part of the object's identity. A name, an identifier, a resource group, a region, a project.
Anything decided at creation and fixed after it. A machine image, an engine, an encryption key, a character set, an availability zone on a resource that does not support moving.
Anything about where the object lives. A subnet, a virtual network, a placement group.
A provider upgrade can change which group an attribute is in, in either direction, because the schema belongs to the provider version you have pinned. That is one of the reasons a plan is worth reading after a provider bump even when no configuration changed.
A replacement is not a destroy
The difference is the create that follows, and it is why a replacement is easier to approve than it should be. A destroy takes the object away and nothing comes back. A replacement takes it away and builds a new one from your configuration, so the plan looks like a change rather than a loss.
What actually survives is only what is in your configuration.
The identity does not. The new object has a new identifier, and every attribute the provider generates is generated again. Anything holding the old value, inside Terraform or outside it, is now holding a value for an object that no longer exists.
The contents do not. Rows in a database, objects in a bucket, files on a volume, anything written by a process rather than by your configuration. The new object is empty.
Time does not. Warm caches, established connections, accumulated logs, anything the object earned by having been up.
This is the whole reason a forced replacement of a database is a different event from a forced replacement of a network interface, even though a plan prints them identically and counts them the same way in its summary.
create_before_destroy, and when it cannot help
By default Terraform destroys the old object and then creates the new one, so there is a gap with nothing in it. The resource can ask for the other ordering:
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+/- create replacement and then destroy
Terraform will perform the following actions:
# aws_instance.web must be replaced
+/- resource "aws_instance" "web" {
~ ami = ... -> ... # forces replacement
~ id = ... -> (known after apply)
...
}
Plan: 1 to add, 0 to change, 1 to destroy.
It is the same replacement and the same data loss. The only thing that changed is that something is serving for the length of the change.
It cannot always be done, and the reasons are worth knowing.
Anything the two objects would both claim stops it. A unique name, a fixed port, a static address, a volume that only attaches to one machine. The new object fails to create while the old one is still holding the thing, and the apply stops there.
The inverted ordering propagates. Terraform applies it to that resource's dependencies too, so one lifecycle block changes the ordering of a part of the graph nobody was looking at. It is not a local setting.
It does not reduce the change. Two objects exist at once, which means two sets of costs, two sets of quota and, on anything stateful, two sources of truth for as long as the apply runs.
What to do about it
The plan is the provider telling you it cannot get to the value you asked for any other way. So the first question is not how to suppress it.
Decide whether the attribute change is worth it. Often the answer is no and the attribute was changed by accident, in a variable default or a module upgrade, rather than on purpose. Put it back and the replacement goes with it.
Check whether the value drifted rather than changed. If something outside Terraform set the value and it is not yours to correct, ignore_changes stops the plan acting on it:Terraform
lifecycle {
ignore_changes = [ami]
}
This is a decision to stop managing that attribute, not a fix, and it is worth writing down beside the block why it is there.
Build the new object alongside, under a new address. Where the resource holds something that has to be migrated rather than rebuilt, a replacement is the wrong instrument entirely. Add the new resource, move what it needs, cut over, then remove the old one as its own change.
Guard the ones that must never go.prevent_destroy in a resource's lifecycle block fails the plan rather than replacing it:terraform plan, abridged
Error: Instance cannot be destroyed
on main.tf line 1:
1: resource "aws_db_instance" "billing" {
Resource aws_db_instance.billing has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed. To avoid this error and
continue with the plan, either disable lifecycle.prevent_destroy or reduce
the scope of the plan using the -target option.
It is a guard and not a fix: the plan stops and the change is still waiting for you. And because the guard lives inside the resource block, deleting the resource from your configuration deletes the guard with it, which is exactly the case it was put there for. It protects against a bad diff, not against a deletion.
What none of these do is turn a replacement into an update. If the attribute is not updatable in the provider's schema, no lifecycle setting makes it so.
Check the plan again before you apply it
Whatever you changed, the plan is what says whether it worked, and it is a different question from whether the configuration is valid.
What a correct plan looks like. That resource is no longer under a replacement symbol at all. There is no forces replacement comment anywhere in its body, the header above it says it will be updated in place or it is absent entirely, and the summary no longer counts it as one to destroy.
What a still-wrong plan looks like. The replacement symbol is still there. Nearly always that means a second attribute also carried the comment and only one was dealt with, because every forcing attribute is a sufficient reason on its own. Search the body again rather than assuming the one you found was the only one.
If the replacement is still there and you cannot find a second forcing attribute, read the header line above the resource. A replacement that arrived from -replace, from a taint or from replace_triggered_by says so there, and none of those three is fixed by editing an attribute.
Reading a plan somebody else made
In a plan you wrote, a forced replacement is expected and the attribute is usually the one you just edited. In a plan that arrives in a pull request it is the opposite: the replacement is the surprise and the attribute is buried in a resource body a few hundred lines long, under a header that reads the same whether the object is a network interface or the production database.
terrakit reads the plan and puts the three facts side by side: what is being replaced, the reason Terraform gave for it, and the attribute path that forced it.
terrakit plan.json
terrakit 1 finding terraform 1.9.8
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CRITICAL ────────────────────────────────────────────────────────────────── 1
azurerm_postgresql_flexible_server.main
destroy and create
├ holds data, so destroying it loses that data
├ an attribute changed that cannot be updated in place
└ forces replacement zone
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 critical
The reason is Terraform's own, read out of the plan rather than inferred, which is why it distinguishes a replacement caused by the diff from one somebody requested or a taint caused. The escalation to critical is the only judgement the tool makes, and it applies to one question: does destroying this resource type lose data.
Notice what is not in that output. The attribute that forced the replacement is named as zone, and neither its old value nor its new one appears, because no value ever does.
Questions about forced replacement
What does forces replacement mean in a Terraform plan?
It means the attribute on that line cannot be changed on the existing object, so the provider has to destroy the object and build a new one to get the value you asked for. Terraform prints the comment against the attribute that did it, and the plan header above the resource says the resource must be replaced.
How do I find which attribute forces replacement?
Search the plan for the comment forces replacement, which Terraform prints at the end of the line holding the attribute responsible. In the JSON form of the plan the same information is the replace_paths field on the resource change, which is a list of attribute paths and is much easier to search when the plan runs to thousands of lines.
Is a replacement the same as a destroy?
No, and the difference is the create that follows. A destroy takes the object away and nothing comes back. A replacement takes it away and builds a new one from your configuration, which means a new identity: a new id, and new values for every attribute the provider generates. Anything the old object held that is not in your configuration does not come across.
What is the difference between -/+ and +/- in a Terraform plan?
The symbol is the ordering. -/+ is destroy and then create replacement, which is the default, and the resource is gone for the length of the apply. +/- is create replacement and then destroy, which is what lifecycle create_before_destroy asks for. The summary line at the end of the plan counts the same one to add and one to destroy either way, so only the symbol tells you which ordering you are getting.
Does create_before_destroy avoid the outage?
Sometimes, and only where the old object and the new one can exist at once. Anything with a uniqueness constraint that the two would both claim, such as a name, a fixed port or a single-attachment volume, fails to create while the old one is still there. Terraform also propagates the inverted ordering to that resource's dependencies, so one lifecycle block can change the ordering of a part of the graph you were not thinking about.
How do I stop Terraform replacing a resource?
Decide first whether the attribute change is worth it, because the replacement is the provider telling you it cannot be done any other way. If it is not, revert the attribute. If the value drifted outside Terraform and is not yours to correct, ignore_changes in a lifecycle block stops the plan acting on it. prevent_destroy is a guard rather than a fix: it fails the plan with Instance cannot be destroyed, and because it lives inside the resource block, deleting the resource deletes the guard with it.