Zero-Downtime Resource Replacement
Some changes cannot be applied in place: a new AMI, a changed subnet, any immutable attribute forces Terraform to replace the resource rather than update it. The default replacement order is destroy-then-create — Terraform tears down the old object before building the new one — and for a resource serving live traffic, that ordering is an outage measured in the time it takes to provision a replacement.
Zero downtime is not one setting; it is a combination. create_before_destroy reorders the swap, load-balancer health checks make sure traffic only reaches healthy instances, and ASG instance refresh rolls a whole fleet without dropping capacity. This topic is the practical recipe for changing production resources without dropping a request.
The Replacement Problem
A -/+ line in a plan is the signal: destroy and recreate. It appears whenever you change an argument the cloud treats as immutable — an EC2 instance's AMI, a launch template's network settings — because there is no in-place edit for those, only a new object. By default Terraform destroys first and creates second, so a serving instance is gone for the entire provisioning window of its replacement. Scan every plan for -/+ on anything that takes traffic; that is the line that becomes downtime.
The annotation that tells you why is (forces replacement) next to the changed argument. It names exactly which change is immutable, which is your cue to decide whether this replacement needs a zero-downtime strategy or can take the default destroy-then-create.
create_before_destroy
The foundation of every zero-downtime swap is the create_before_destroy lifecycle setting. It inverts the order: Terraform builds the replacement first, and only destroys the old object once the new one exists. For a resource behind a load balancer, that means the new instance can come up and start serving before the old one goes away, so there is no gap where nothing is handling requests.
resource "aws_instance" "api" { ami = var.api_ami instance_type = "t3.medium" lifecycle { create_before_destroy = true } }
The catch is fixed unique names. If a resource has a name that must be unique — a fixed S3 bucket name, a named launch configuration — create-before-destroy fails, because the new object cannot be created while the old one still holds the name. Either let the name be generated (a name prefix the provider completes) or accept that the resource cannot be create-before-destroy and plan its replacement differently.
Load Balancer Draining
create_before_destroy gets a new instance running, but the load balancer is what actually shifts traffic safely. Target-group health checks hold traffic back from the new instance until it passes a configured number of checks, so requests only flow once it is genuinely ready. Connection draining (deregistration delay) does the reverse on the way out: the old instance stops receiving new connections but keeps serving in-flight requests until they finish or the delay elapses, typically 30 to 300 seconds.
Without health checks, create-before-destroy can route traffic to an instance that booted but has not finished starting its application, turning a zero-downtime swap into a wave of 502s. The health check is what makes "the new instance exists" mean "the new instance is ready," and those are not the same moment.
ASG Instance Refresh
A single instance is the simple case; a fleet behind an Auto Scaling group needs rolling replacement. ASG instance refresh replaces the instances in a group in batches, controlled by a minimum healthy percentage so capacity never drops below a floor you set. Set it to 90% and the refresh replaces roughly one instance at a time, keeping the fleet near full capacity throughout; set it to 50% and it moves faster but runs at half strength mid-refresh.
resource "aws_autoscaling_group" "api" { desired_capacity = 6 max_size = 9 min_size = 6 instance_refresh { strategy = "Rolling" preferences { min_healthy_percentage = 90 } } }
Replacing a whole fleet at once is the mistake this avoids. A 90% floor on a six-instance group means at most one instance is out at a time, so the fleet serves at near-full capacity through the entire roll rather than dropping to zero while every instance swaps together.
Stateful Resources
None of this applies cleanly to a database. create_before_destroy on an RDS instance does not preserve the data — the new instance starts empty, and the old one with all your data is what gets destroyed. Stateful resources need a fundamentally different strategy: snapshots taken before any change, read replicas promoted to take over, or a blue-green deployment that stands up a synchronized copy and cuts over once it has caught up.
Treating a database replacement like a stateless one is how you lose data, not just availability. The stateless recipe optimizes for never dropping a request; a stateful resource optimizes for never dropping a row, and that requires snapshots, replicas, or blue-green — never a naive create-before-destroy.
- Replacing a serving instance without
create_before_destroy, so the default destroy-then-create takes it down for the entire provisioning window of its replacement. - Adding
create_before_destroyto a resource with a fixed unique name, so the new object cannot be created while the old one still holds the name and the swap fails. - Relying on create-before-destroy without load-balancer health checks, so traffic routes to an instance that booted but has not finished starting, producing a wave of 502s.
- Treating a database replacement like a stateless one, so the new instance starts empty and the old instance with all the data is destroyed.
- Replacing a whole fleet at once instead of a rolling ASG instance refresh, dropping capacity to zero during the swap.
- Use
create_before_destroyplus load-balancer health checks for any stateless resource serving traffic, so the replacement is ready before the old one goes away. - Roll fleet replacements with ASG instance refresh and a minimum-healthy-percentage floor rather than replacing every instance at once.
- Avoid fixed unique names on resources you expect to replace, or use a name prefix so create-before-destroy is possible.
- Set a connection-draining deregistration delay so in-flight requests finish on the old instance before it is destroyed.
- Plan stateful replacements separately with snapshots, promoted read replicas, or a blue-green cutover — never a naive create-before-destroy.
Knowledge Check
Why does the default replacement of a serving instance cause downtime?
- Terraform destroys the old instance before creating the new one, so nothing serves during the replacement's provisioning window
- The load balancer is fully detached from all of its registered targets for the entire duration of any apply that touches the instance
- Replacing one instance forces the whole Auto Scaling group to scale down to zero first
- State locking blocks all data-plane traffic to the instances until the apply completes
What does create_before_destroy plus load-balancer health checks achieve together?
- The replacement is built first and only receives traffic once it passes health checks, so there is no gap in serving
- The old instance keeps serving traffic forever and the freshly built replacement is created but then never actually used
- Terraform skips the replacement entirely and quietly updates the AMI in place instead
- The attached database is snapshotted automatically right before the swap occurs
Why can a resource with a fixed unique name break create_before_destroy?
- The new object cannot be created while the old one still holds the unique name, so create-before-destroy fails on the collision
- A fixed name silently disables the load-balancer health checks on that target, so traffic never shifts over to the new instance
- Terraform refuses to accept a lifecycle block on any resource that has a hard-coded name
- The load balancer cannot register two separate targets that share the same name
Why does a database need a different replacement strategy than stateless compute?
- Create-before-destroy starts the new instance empty and destroys the old one with the data, so you need snapshots, replicas, or blue-green
- Databases cannot sit behind a load balancer at all, so the health-check gating approach that works for compute simply does not apply to them
- RDS replacements are always performed in place and never show a
-/+in the plan - ASG instance refresh handles managed database fleets exactly the same way it handles compute fleets
You got correct