TerraformVII · Resources, Data Sources, and count/for_eachProduction Terraform
Resource Replacement and In-Place Updates
What you'll learn
- Distinguish in-place update from forced replacement in the plan output
- Identify which provider schema attributes are immutable (ForceNew)
- Use -replace and lifecycle.replace_triggered_by to control replacement
- Configure create_before_destroy to avoid downtime during replacement
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A team needs to change the instance class of an RDS database. They
edit the configuration, run terraform plan, and see -/+. They
expected ~. They approve the plan. Production goes down for ten
minutes while RDS tears down the old instance and provisions a new
one. The mistake was not a misconfiguration; it was a misreading of
the plan. This lesson is the operational discipline around
replacement.
Update versus replace
When a configuration attribute changes, Terraform consults the provider schema to decide between two operations:
- In-place update (
~). The provider’sUpdateAPI is called. The resource persists; only the changed attribute is mutated. There is no downtime from the provider’s perspective, though the resource may briefly enter a modifying state (e.g., an EC2 instance rebooting for an EBS volume resize). - Forced replacement (
-/+or+/-). The provider’s attribute is markedForceNew: truein its schema. The provider has noUpdateAPI for that attribute; the only way to change it is to destroy the old resource and create a new one. The provider decides this, not Terraform.
Configuration change
|
v
+-------------------+ Yes +-----------------+
| Attribute marked | --------------> | Replace ( -/+ )|
| ForceNew in schema| +-----------------+
+-------------------+
| No
v
+-------------------+ Yes +-----------------+
| Provider has an | --------------> | Update ( ~ ) |
| Update API for it | +-----------------+
+-------------------+
| No
v
+-------------------+
| Replace anyway |
| (impossible) |
+-------------------+
Real example. An EC2 instance:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
}
instance_type— mutable. Changingt3.smalltot3.mediumtriggersUpdate.ami—ForceNew: true. Changing the AMI triggers-/+.subnet_id—ForceNew: true. Moving a subnet triggers-/+.
To see this in the schema:
terraform providers schema -json | \
jq '.provider_schemas."registry.terraform.io/hashicorp/aws".resource_schemas."aws_instance".block.attributes.ami'
{
"type": "string",
"required": true,
"force_new": true
}
The force_new key is the field that controls the decision.
The -replace flag
Terraform 0.15.2 introduced -replace (alias: -target for
replacement). It forces a replacement regardless of whether the
attribute is ForceNew. Use it for: state-only refreshes of a
specific resource, or to recover from drift that requires a rebuild.
terraform apply -replace="aws_instance.web"
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ ami = "ami-0c55b159cbfafe1f0" -> "ami-0a1b2c3d4e5f67890" # forces replacement
id = "i-0abcd1234ef567890"
# (16 unchanged attributes hidden)
}
The CLI flag forces the action even if the configuration did not change. The provider then performs a destroy on the old instance and a create of the new one.
The lifecycle block
The lifecycle block is the right place to control replacement
behaviour. Three meta-arguments matter for this lesson:
create_before_destroy = true— reverses the order. Create the new resource first, then destroy the old. Essential for stateful resources where the new instance must be ready before the old one is torn down.prevent_destroy = true— blocks any plan that would destroy the resource. Use for production databases, S3 buckets with object lock, KMS keys.replace_triggered_by = [...]— forces replacement when the referenced resource or attribute changes. Use to model cascading changes (e.g., rotate a secret and rebuild the database).
resource "aws_db_instance" "primary" {
identifier = "prod-db"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.r6g.large"
allocated_storage = 100
storage_type = "gp3"
lifecycle {
create_before_destroy = true
prevent_destroy = true
}
}
resource "aws_iam_role" "app" {
name = "app-role"
lifecycle {
replace_triggered_by = [
aws_iam_policy.app.arn
]
}
}
The combination create_before_destroy = true plus
prevent_destroy = true is the canonical pattern for production
databases. The first rule ensures the new instance is ready before
the old is torn down; the second rule blocks an accidental
destruction by a misread plan.
Reading replacement in the plan
Three patterns to memorise:
# In-place update — no destroy
~ resource "aws_instance" "web" {
~ instance_type = "t3.small" -> "t3.medium"
}
# Forced replacement — destroy first, then create (the default)
-/+ resource "aws_instance" "web" {
~ ami = "ami-old" -> "ami-new" # forces replacement
}
# Forced replacement with create_before_destroy — create first, then destroy
+/- resource "aws_instance" "web" {
~ ami = "ami-old" -> "ami-new" # forces replacement
}
The difference between -/+ and +/- is the order of operations.
For stateful resources, +/- is almost always what you want, because
it avoids the gap where neither the old nor the new resource exists.
Production failure modes
-
Misreading
~for-/+. The plan symbol is-/+, but the operator reads it as~(in-place). Production is destroyed. Symptom: the apply succeeds with no errors, but the resource is a new instance with a new ID. References that depended on the old ID are stale. Recovery: restore from state backup taken before the apply; if the new resource is acceptable, runterraform state showand update the dependents. -
No
create_before_destroyon a stateful resource. An RDS instance is replaced with-/+. There is a ten-minute gap where the database is unavailable. Symptom: application health checks fail during the apply window. Recovery: addcreate_before_destroy = trueto the lifecycle block; verify withterraform planthat the symbol is now+/-. -
Replacing a resource with
instance_storedata. The provider marksinstance_typeasForceNew. The replacement destroys the instance, losing all instance-store data. Symptom: application data missing on the new instance. Recovery: ensure data is on EBS, not instance store, before changing instance class. -
replace_triggered_bypointing at the wrong resource. The lifecycle references a resource that changes on every plan. Every plan now shows-/+. Symptom: apply times grow because every dependent resource rebuilds. Recovery: narrow the trigger to the specific attribute that should cause a rebuild. -
prevent_destroy = trueblocking a legitimate teardown. A team needs to destroy an environment as part of a decommission. The lifecycle block prevents the destroy. Symptom:Error: Instance cannot be destroyed. Recovery: remove the lifecycle block (or setprevent_destroy = false) and apply; restore the block afterwards if the resource is still needed. -
Concurrent applies during a replacement. Two operators approve a replacement at the same time. The state lock blocks the second, but the first runs to completion. The second, when unblocked, attempts to replace again. Symptom: the resource is replaced twice; the state file references an instance that no longer exists. Recovery:
terraform plan -refresh-onlyto reconcile;terraform state rmfollowed byterraform importif the live resource needs to be re-adopted.
Security and performance
Security. Replacement destroys the old resource. If the resource
holds data (database, EBS, S3, KMS key), the destroy step must be
audited. For S3 buckets with object lock, the destroy fails; for
databases without a final snapshot, the data is lost. Always
configure skip_final_snapshot = false for RDS.
Performance. A replacement is at minimum one Create + one
Delete. For large resources (RDS, ElastiCache, EFS), this is
expensive. Use create_before_destroy = true so the new resource
is provisioned before the old is torn down, but accept that the
provisioning time is doubled.
What to do in production
- Read the plan symbol.
-/+and+/-are destructive.~is not. There is no ambiguity. - For stateful resources (databases, caches, queues), always set
create_before_destroy = true. For objects that must not be destroyed (data buckets, compliance logs), always setprevent_destroy = true. - Test replacement in a non-production environment first. Watch the apply. Confirm the new resource comes up before the old is torn down.
- Run
terraform plan -replace=...beforeterraform apply -replace=.... The plan output is the same as the apply output. - For databases, configure automated snapshots before any replacement.
Verification
# 1. Inspect which attributes are ForceNew for a resource
terraform providers schema -json | \
jq '.provider_schemas."registry.terraform.io/hashicorp/aws".resource_schemas."aws_db_instance".block.attributes | to_entries[] | select(.value.force_new == true) | .key'
# 2. Plan a change and confirm the replacement symbol
terraform plan -out=tfplan
terraform show -json tfplan | jq '.resource_changes[] | select(.change.actions | length > 1) | {address: .address, actions: .change.actions}'
# 3. Confirm lifecycle is configured as expected
grep -A3 'lifecycle' main.tf
# 4. Verify the resource is healthy after a replacement
terraform state show aws_db_instance.primary
aws rds describe-db-instances --db-instance-identifier prod-db
# 5. Confirm the new resource is the one referenced in state
terraform refresh
terraform plan # should be empty
A clean verification:
$ terraform plan
No changes. Your infrastructure matches the configuration.
The plan is empty. The resource that was just replaced is the same resource in state that the configuration describes.
Knowledge check · 7 questions
Q1. What does `ForceNew: true` in a provider schema attribute mean?
Q2. What does `create_before_destroy = true` do?
Q3. Terraform decides whether a change requires replacement.
Q4. Which of the following are valid lifecycle meta-arguments in Terraform 1.9.x? (Select all that apply.)
Q5. What is the difference between the plan symbols `-/+` and `+/-`?
Q6. What does `terraform apply -replace="aws_instance.web"` do?
Q7. A production RDS instance must be resized from db.r6g.large to db.r6g.xlarge. The team runs `terraform plan` and sees `-/+`. They want no downtime. What is the minimum change required?
Passing score: 75%. Answers are checked in this browser.