Skip to content
Infrastructure as Code iac workflow 4 min read

Taint & Replace

Sometimes a resource is technically healthy in state but practically broken — a VM that drifted into a bad state, a database whose bootstrap script failed silently, or an instance you simply want rebuilt from scratch. Terraform’s configuration hasn’t changed, so a normal plan shows nothing to do. To force a destroy-and-recreate of that one resource, you use terraform apply -replace=ADDR. This is the modern, plan-visible replacement for the old terraform taint command, and learning when (and when not) to reach for it keeps your infrastructure declarative rather than hand-managed.

Why force recreation at all

Terraform’s whole model is convergence: you declare the desired state, and Terraform makes reality match. When config and reality already agree, there is nothing to apply. But there are real situations where the resource exists, matches its arguments, and is still wrong:

  • A bootstrap user_data script failed, so the instance booted but never configured itself.
  • A resource drifted into a corrupted or stuck state out-of-band.
  • A managed resource needs to be cycled to pick up an externally changed image or AMI without changing its Terraform arguments.
  • You want a clean rebuild to rule out accumulated manual changes.

In all of these, you don’t want to edit the configuration — the desired config is correct. You just need this specific object thrown away and built again.

The modern way: apply -replace

-replace takes a single resource address and tells Terraform to plan a replacement for it: destroy the existing object, then create a new one in its place. Crucially, the replacement appears in the plan before you approve, so you see exactly what will happen.

terraform apply -replace="aws_instance.app"

For a resource using count or for_each, include the index or key in the address:

terraform apply -replace='aws_instance.app[0]'
terraform apply -replace='aws_instance.app["web"]'

Output:

Terraform will perform the following actions:

  # aws_instance.app will be replaced, as requested
-/+ resource "aws_instance" "app" {
      ~ id                     = "i-0abc123def456" -> (known after apply)
        ami                    = "ami-0c55b159cbfafe1f0"
      ~ private_ip             = "10.0.1.27" -> (known after apply)
        instance_type          = "t3.micro"
        # (8 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Enter a value: yes

The -/+ symbol means “destroy then create” — replacement. The trailing comment as requested confirms the replacement came from your -replace flag rather than from a config change. The configuration that backs this is unchanged:

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "app-server"
  }
}

You can also preview the replacement without committing by passing the flag to plan:

terraform plan -replace="aws_instance.app"

OpenTofu supports the identical syntax: tofu apply -replace=ADDR. Everything on this page applies to both tools.

The deprecated way: terraform taint

Before Terraform 0.15.2, you marked a resource for recreation with a separate, stateful command:

terraform taint aws_instance.app   # deprecated
terraform untaint aws_instance.app # to undo

taint mutated state immediately and invisibly — there was no plan to review at the moment you ran it. The recreation only showed up on your next plan/apply, and forgetting an outstanding taint led to surprise rebuilds. -replace fixes this by making the intent a one-shot flag on the operation that actually runs.

Aspectterraform taint (legacy)apply -replace (modern)
StatusDeprecated since 0.15.2Recommended
When it modifies stateImmediately, before any planOnly during the apply you run
Plan visibilityNone at taint timeFull diff shown before approval
ReversibleNeeds untaintNothing to undo — it’s per-run
Works in CI safelyAwkward (stateful side effect)Yes (explicit, atomic)

If you still see taint in scripts or older docs, replace it with -replace. There is no behavioral reason to keep using it.

Combining with other flags

-replace composes with the usual apply flags. A common combination is narrowing the run with -target so the replacement happens in isolation, or chaining several -replace flags to cycle multiple resources at once:

terraform apply -replace="aws_instance.app[0]" -replace="aws_instance.app[1]"

You can also save a replacement plan and apply it later — useful in pipelines that separate review from execution:

terraform plan -replace="aws_instance.app" -out=replace.tfplan
terraform apply replace.tfplan

Replacement vs. lifecycle-driven recreation

Don’t confuse a manual -replace with the replacements Terraform plans on its own. When you change an attribute that the provider marks as force new (for example, an EC2 availability_zone), Terraform plans a -/+ automatically — that belongs in your config and your version history. Use -replace only for the cases where config is correct but the live object is bad. If you find yourself replacing the same resource repeatedly, that’s a signal to fix the configuration (or add create_before_destroy in a lifecycle block) instead of forcing it by hand each time.

Best Practices

  • Prefer terraform apply -replace=ADDR everywhere; treat terraform taint as deprecated and remove it from scripts.
  • Run terraform plan -replace=ADDR first so you see the -/+ diff before destroying anything.
  • Quote resource addresses that contain [, ], or " so your shell doesn’t mangle them.
  • Use -replace for “the object is broken,” not for changes that belong in configuration — config-driven changes should live in code.
  • For zero-downtime rebuilds, add a lifecycle { create_before_destroy = true } block so the new resource exists before the old one is destroyed.
  • In CI, save the replacement plan with -out and apply that exact file, keeping review and execution atomic.
  • If a resource needs replacing on every run, fix the root cause rather than scripting a recurring -replace.
Last updated June 14, 2026
Was this helpful?