Skip to content

Resource Lifecycle and Replacement


One of the most important moments in a Terraform plan is the moment you see a resource marked for replacement. Terraform is telling you that the existing object cannot simply be updated to match the new configuration, so it needs to create a new object and remove the old one.

Understanding why that happens is much more useful than memorizing lifecycle syntax.

Update or Replacement?

When you change configuration, Terraform asks the provider what can be changed on the existing object.

Conceptually, there are two possibilities:

Configuration change
        β”‚
        β–Ό
Can existing object be updated?
      /   \
    yes    no
    β”‚       β”‚
    β–Ό       β–Ό
Update   Replace
in place

An in-place update might look conceptually like:

server A
   β”‚
   β”‚ modify
   β–Ό
server A

A replacement looks like:

server A
   β”‚
   β”‚ destroy/create
   β–Ό
server B

The exact behavior depends on the resource and provider.

For some attributes, the provider API supports modification. For others, the underlying infrastructure system requires a new object.

Why Would a Resource Need Replacement?

Some infrastructure properties are effectively immutable.

Imagine a resource where changing an argument means the provider cannot modify the existing object:

resource "some_provider_server" "web" {
  name = "web"
  size = "small"
}

If changing size requires a new server, a plan might indicate something conceptually like:

-/+ some_provider_server.web

      size: "small" -> "large"

The -/+ marker indicates replacement: destroy and create.

Terraform is not arbitrarily deciding to recreate the server. It is following the resource’s behavior as defined by the provider.

Note

Whether a change is in-place or replacement is resource-specific. Always read the plan rather than assuming that changing a particular argument will behave one way across all providers.

Read the Plan Carefully

A plan can contain several different kinds of changes.

Conceptually:

+     create
~     update in place
-     destroy
-/+   replace

The exact plan formatting can contain additional details, but the distinction is important.

For a normal update:

~ resource

Terraform expects to modify the existing object.

For replacement:

-/+ resource

Terraform expects the old object to be replaced.

Replacement may cause downtime, change addresses, rotate identifiers, or cause dependent resources to change as well.

The lifecycle Block

Terraform provides a lifecycle block for controlling certain aspects of resource lifecycle behavior.

For example:

resource "some_provider_server" "web" {
  name = "web"

  lifecycle {
    create_before_destroy = true
  }
}

The important lifecycle settings include:

  • create_before_destroy
  • prevent_destroy
  • ignore_changes

They are not general-purpose “make Terraform safer” switches. Each changes Terraform’s behavior and introduces trade-offs.

create_before_destroy

Normally, replacement can conceptually happen like this:

Old resource
     β”‚
     β–Ό
  Destroy
     β”‚
     β–Ό
  Create new

That creates a period where the old resource is gone.

With:

lifecycle {
  create_before_destroy = true
}

Terraform attempts the opposite ordering:

Old resource
     β”‚
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚              β–Ό
     β”‚         Create new
     β”‚              β”‚
     β”‚              β–Ό
     └────────── Destroy old

This can reduce downtime.

However, it only works when the infrastructure system allows both objects to exist simultaneously.

If a resource requires a globally unique name, for example, Terraform may not be able to create the replacement before destroying the original.

Warning

create_before_destroy does not guarantee zero downtime. The provider and infrastructure design must allow both old and new objects to coexist.

It can also temporarily increase resource usage because two objects exist during replacement.

prevent_destroy

You can protect a resource with:

lifecycle {
  prevent_destroy = true
}

Terraform will reject operations that would destroy that resource through normal Terraform actions.

This can be useful for resources where accidental destruction would be particularly serious.

For example:

resource "some_provider_database" "main" {
  name = "production"

  lifecycle {
    prevent_destroy = true
  }
}

The purpose is not to make the resource impossible to delete forever. It is to force an explicit decision rather than allowing an ordinary plan to destroy it.

Note

prevent_destroy can also make legitimate changes fail when those changes require replacement. It is a guardrail, not a substitute for backups and operational discipline.

ignore_changes

Sometimes an external system is intentionally allowed to modify part of a resource.

You can tell Terraform to ignore changes to selected attributes:

resource "some_provider_server" "web" {
  name = "web"

  lifecycle {
    ignore_changes = [
      metadata
    ]
  }
}

Terraform will avoid planning changes solely because the ignored attribute differs from the configuration.

This can be appropriate when another system intentionally owns that attribute.

But it can also hide drift.

Imagine your configuration declares:

size = "small"

and you add:

ignore_changes = [size]

Now an administrator can change the size outside Terraform and Terraform will intentionally stop trying to reconcile that difference.

That may be correctβ€”or it may hide a real configuration problem.

Warning

ignore_changes should answer a specific ownership question: “Who is responsible for this attribute?” If the answer is still Terraform, ignoring it is usually the wrong solution.

Lifecycle Settings Change the Rules

It is useful to think of lifecycle settings as policy decisions:

                 Resource change
                       β”‚
                       β–Ό
                Terraform plan
                       β”‚
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β–Ό                   β–Ό
        Normal lifecycle    Custom lifecycle
                                 β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β–Ό            β–Ό            β–Ό
              create first   block destroy  ignore

The settings exist because infrastructure operations sometimes have requirements that cannot be represented by the default lifecycle.

But every customization makes Terraform’s behavior less automatic.

A Small Replacement Exercise

Create a deliberately simple configuration using a provider/resource where one chosen argument is documented to require replacement.

For example:

replacement-example/
└── main.tf

Use the provider’s documentation to choose a resource whose immutable attribute makes the behavior obvious.

Your workflow is:

terraform init
terraform plan
terraform apply

Then change the replacement-triggering argument and run:

terraform plan

Read the plan before applying it.

Your goal is not merely to see the replacement marker. Ask:

  • Which attribute changed?
  • Why can the provider not update it in place?
  • Will the replacement cause downtime?
  • Does the resource have a stable identity?
  • Do dependent resources also need changes?

Finally, clean up:

terraform destroy

The exact resource used for this exercise depends on the provider you are practicing with, so do not copy a made-up provider resource from a generic tutorial into a real environment.

Lifecycle Is About Trade-Offs

A common beginner mistake is to discover create_before_destroy, prevent_destroy, and ignore_changes and assume they should be added everywhere.

They should not.

Think about the trade-offs:

SettingUseful ForMain Trade-Off
create_before_destroyReducing replacement downtimeRequires old and new objects to coexist
prevent_destroyGuarding important resourcesCan block legitimate replacement
ignore_changesAttributes intentionally managed elsewhereCan hide drift

The right question is not:

“Which lifecycle option makes Terraform safer?”

It is:

“What lifecycle behavior does this resource actually require, and who owns each change?”

That mindset prevents lifecycle blocks from becoming a collection of unexplained exceptions.

The next chapter moves from lifecycle behavior into Terraform’s expression language, where functions, collection transformations, and dynamic blocks let you build more flexible configurations.

Last updated on