The two commands, and the one that superseded the first of them:
terraform taint aws_instance.webterraform untaint aws_instance.webterraform apply -replace=aws_instance.webWhat tainted means
Tainted is a mark on one entry in the state file. Not on the object in your cloud account, which is untouched and does not know about it, and not in your configuration, where there is nothing to see. Terraform's own description is a resource instance that may not be fully functional, and the mark is how it records that suspicion between one run and the next.
There are two ways an instance gets marked:
- Terraform sets it. A create that failed partway through leaves an object that exists and may not work. Terraform cannot tell which, so it records the doubt rather than pretending the resource is fine.
- You set it, with
terraform taint, because you know something the state does not: the box was patched by hand, a bootstrap script failed silently, the instance is in a state you would rather replace than debug.
The address is an ordinary instance address, so aws_instance.web[1] and module.app.aws_instance.web both work, and the mark applies to that one instance rather than to every instance of the resource.
It is state surgery. The command takes a lock on the state, writes to it, and returns. There is no diff, no pull request and nothing in the repository to say it happened. The next person to plan this workspace gets a replacement they did not ask for and cannot explain from the code, which is the real cost of the command and the reason it has a successor.
What it does to the next plan
A tainted instance is planned for replacement, and Terraform names the reason in the header above the resource:
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 is tainted, so must be replaced
-/+ resource "aws_instance" "web" {
~ id = ... -> (known after apply)
...
}
Plan: 1 to add, 0 to change, 1 to destroy.Three things in that output are worth reading properly.
The symbol is the ordering. -/+ is destroy and then create replacement, so the object is gone for the length of the apply. +/- is the other way round and you only get it where the resource asks for it with create_before_destroy. Terraform prints the legend at the top of every plan, and it is the difference between an outage and a cutover.
The summary line does not tell you. One to add and one to destroy is what a replacement counts as, and it is also what an unrelated create plus an unrelated destroy counts as. Nothing in that line says the two are the same object.
A taint on a resource that holds data is a data loss. The replacement builds a new object from your configuration, so anything the old one held that is not in the configuration does not come across. On a virtual machine that is usually the point. On a database, a bucket or a volume it is the thing you were trying to avoid, and it arrives without anybody typing the word destroy.
What to use instead
Terraform 0.15.2 added -replace, and HashiCorp's documentation has pointed at it rather than at terraform taint ever since. It does the same job from the other end: instead of marking state and letting the next plan discover the mark, it asks for the replacement at plan time.
terraform apply -replace=aws_instance.webThe header in the resulting plan says so, and says it differently from a replacement Terraform decided on by itself:
# aws_instance.web will be replaced, as requested
-/+ resource "aws_instance" "web" {
~ id = ... -> (known after apply)
...
}
Plan: 1 to add, 0 to change, 1 to destroy.Nothing is written to state until the plan is approved. That is the whole of the difference and it is worth the switch on its own. A taint you thought better of has to be undone with another state write. A -replace you thought better of is a plan you did not apply.
The option can be given more than once, so replacing three instances is one command rather than three state writes:
terraform apply -replace=aws_instance.web -replace=aws_instance.workerAnd it works with a saved plan, which is the shape worth using anywhere the change is reviewed rather than typed at a prompt:
terraform plan -replace=aws_instance.web -out tfplanterraform apply tfplanThe plan file is the artefact. It can be read, shown as JSON, attached to a pull request and applied exactly as reviewed, and none of that is available for a change that has already been made to the state.
The taint command still exists in current Terraform, still works, and prints no deprecation warning when you run it. That is why it is still in runbooks written years ago, and it is the reason this page exists rather than a redirect to the release notes.
Check the plan before you apply it
This is the step that decides whether the problem is fixed, and it is not the same question as whether the command was accepted. Both terraform taint and terraform apply -replace report success for work that is about to do something you did not intend, because both of them succeeded at what you asked.
What a correct plan looks like. Exactly one resource under a replacement symbol, and it is the address you named. The header above it says is tainted, so must be replaced if you used taint, or will be replaced, as requested if you used -replace. The summary is one to add and one to destroy per resource you meant to replace, and nothing else.
What a still-wrong plan looks like. Any of these means stop rather than apply:
- A second resource is being replaced or destroyed. Something depends on the one you named, or the taint landed on an address you did not mean. Read the header line above each resource: a replacement Terraform decided on by itself has a different reason printed there from the one you asked for.
- A resource that holds data is in the list. A database, a bucket, a volume, a key vault. That is not a question about syntax, it is a question about whether this change is worth the data, and it needs answering before the apply rather than during it.
- The address you named is not in the plan at all. The taint went to a different workspace or a different state, or the address has a typo that matched nothing.
terraform state listwill tell you which addresses actually exist.
If a destroy is still there and you do not want it, nothing has happened yet, and that is the point of stopping here. A plan is not an apply. With -replace there is nothing to undo: discard the plan and the state is as it was. With terraform taint the mark is already written, so clear it with terraform untaint on the same address, then plan again and confirm the replacement has gone.
When you still need untaint
terraform untaint is not deprecated, and the reason is worth knowing: the mark it clears is not always one you set. Terraform applies it on its own after a create that failed partway through, and untaint is the only way to say the object turned out to be fine without replacing it.
terraform untaint aws_instance.webSo the honest division is that -replace replaces taint, and nothing replaces untaint. There is no inverse of -replace because there is nothing to reverse: a plan you did not apply left no trace.
Untaint is state surgery on the same terms as taint. It takes a lock, writes to the state, and leaves nothing in the repository to say it happened. Before you run it, be sure the object really is healthy: the mark exists because Terraform could not confirm that, and clearing it is you taking that call on Terraform's behalf. Both commands accept -allow-missing, which makes them exit zero when the address is not in the state, and which is only ever what you want in a script.
What taint will not do
- It does not change anything in your cloud account. No API call is made. Marking a resource is free; the destroy happens at apply and not before.
- It does not override prevent_destroy. A resource carrying
prevent_destroyin its lifecycle block fails the plan rather than being replaced, and the mark stays where it is until you clear it or remove the guard:terraform plan, abridged Plan: 1 to add, 0 to change, 1 to destroy. 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. - It does not repair anything. The new object is built from your configuration. If the configuration is what was wrong, replacing the resource produces the same broken object with a new identifier.
- It does not tell anyone. No diff, no review, no record. A colleague planning the same workspace inherits your replacement and has nothing in the repository that explains it.
- It is not a rename. If what you actually want is the same object under a different address, that is a moved block, and it destroys nothing.
Spotting one in somebody else's plan
A plan arriving in a pull request carries the reason for every replacement in it, and the reasons are not interchangeable. A resource replaced because an attribute cannot be updated in place is a consequence of the diff in front of you. A resource replaced because it is tainted is a consequence of something somebody did to the state, in a shell, on a day nobody wrote down.
Terraform records that distinction in the machine-readable form of the plan, as an action reason on the resource change. Terraken reads the field rather than guessing at it, and prints the resource is tainted as the replacement reason beside the address, ranked with every other destructive change in the plan and escalated to critical where the resource type holds data.
That is the whole of its involvement here: it names what Terraform already said, in a report you can read in one screen rather than in a plan that runs to thousands of lines. It never runs Terraform, and it cannot taint, untaint or replace anything.