terraken

Terraken

Terraform taint and untaint

terraform taint sets a flag on one resource instance in your state file. Nothing in your cloud account changes when you run it. The effect arrives on the next plan, which destroys that object and creates a new one in its place. terraform untaint clears the flag. The taint command is deprecated in favour of terraform apply -replace=ADDRESS, which plans the same replacement without writing to state first, so you read it before it happens rather than afterwards.

The two commands, and the one that superseded the first of them:

terraform taint aws_instance.web
terraform untaint aws_instance.web
terraform apply -replace=aws_instance.web

What 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 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 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.web

The header in the resulting plan says so, and says it differently from a replacement Terraform decided on by itself:

terraform plan, abridged
  # 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.worker

And 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 tfplan
terraform apply tfplan

The 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 list will 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.web

So 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_destroy in 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.

Questions about taint

Is terraform taint deprecated?
Yes. HashiCorp's documentation has pointed at the -replace option on terraform apply instead of terraform taint since Terraform v0.15.2. The command still exists, still works and prints no deprecation warning when you run it, which is why it is still in so many runbooks. terraform untaint is a different case and is not deprecated, because Terraform still sets the tainted mark itself when a create fails partway through.
What does terraform taint actually do?
It sets a flag on one resource instance in the state file, and that is the whole of it. No API call is made and nothing in your cloud account changes. The effect arrives on the next plan, which sees a tainted instance and plans to destroy that object and create a new one in its place.
How do I replace a resource without terraform taint?
Run terraform apply -replace=ADDRESS, or terraform plan -replace=ADDRESS -out tfplan if you want the plan reviewed before it is approved. The option can be given more than once to replace more than one object. The difference that matters is that nothing is written to state until the plan is approved, where terraform taint writes to state first and shows you the consequence afterwards.
What is the difference between terraform taint and terraform untaint?
terraform taint sets the tainted mark on a state entry, so the next plan replaces that object. terraform untaint clears the mark, so the next plan leaves it alone. Both are state surgery: they take a lock, write to the state, and leave nothing in the repository to say it happened. Neither one touches your cloud account.
Does terraform taint destroy anything immediately?
No. It writes a flag to state and returns. The destroy happens at the next apply, and the plan before it says so: the resource is headed is tainted, so must be replaced, and the summary counts one to add and one to destroy. If that resource has prevent_destroy set in its lifecycle block, the plan fails with Instance cannot be destroyed and the mark stays where it is.
Can I taint one instance of a count or for_each resource?
Yes. The address is an ordinary instance address, so aws_instance.web[1] and module.app.aws_instance.web are both valid, and the mark lands on that one instance rather than on every instance of the resource. terraform apply -replace takes the same addresses.

Terraken uses Google Analytics to count how many people read these pages. No cookie is set unless you accept, and every page works exactly the same either way.