Lifecycle & Dependencies
Timing, replacement, count vs for_each, moved blocks.
Lifecycle & Dependencies
Once you're past the basics, the trickiest Terraform questions are about timing: which order do resources change in, when does Terraform destroy-then-recreate vs update in place, and how do you stop an apply from doing the wrong thing.
Implicit dependencies
Most dependencies are implicit. When resource A's config references resource B's attribute, Terraform knows A must be created after B:
resource "aws_security_group" "web" { ... }
resource "aws_instance" "web" {
vpc_security_group_ids = [aws_security_group.web.id] # implicit dependency
}
The graph builds itself from references. You almost never need to declare dependencies by hand.
Explicit depends_on
When the dependency is real but Terraform can't see it (e.g., one resource creates a side effect another reads), use depends_on:
resource "aws_iam_role_policy_attachment" "lambda" {
role = aws_iam_role.lambda.name
policy_arn = aws_iam_policy.lambda.arn
}
resource "aws_lambda_function" "main" {
# ...
depends_on = [aws_iam_role_policy_attachment.lambda]
}
Without depends_on, Terraform might create the Lambda before the policy attachment exists; AWS would then fail or the function would run with insufficient permissions on first invocation.
Use sparingly — implicit beats explicit.
count vs for_each
Two ways to create N copies of a resource.
count is integer-indexed:
resource "aws_instance" "web" {
count = 3
ami = "ami-..."
instance_type = "t3.micro"
}
# Reference: aws_instance.web[0], aws_instance.web[1], ...
for_each is keyed:
resource "aws_instance" "web" {
for_each = toset(["us-east-1a", "us-east-1b"])
ami = "ami-..."
instance_type = "t3.micro"
availability_zone = each.key
}
# Reference: aws_instance.web["us-east-1a"], ...
Prefer for_each when the items have meaningful identities. If you remove the middle item from a count = 5 list, every later instance shifts by one index — Terraform sees that as "destroy the last one + recreate everything in between". With for_each, removing one key only destroys that one resource.
lifecycle block
Three useful options:
resource "aws_instance" "web" {
ami = "ami-..."
lifecycle {
create_before_destroy = true # spin up the new one first, then kill the old
prevent_destroy = true # refuse to destroy this resource on apply
ignore_changes = [tags] # don't recreate when this attribute drifts
}
}
create_before_destroy— for resources that need zero downtime when replaced. The new instance is created before the old one is destroyed.prevent_destroy— guard rail for production resources. Ifapplywould destroy this, it errors out instead. Useful on databases, hosted zones, anything irreplaceable.ignore_changes— when something else mutates an attribute (autoscaler, CI tooling) and Terraform should stop "fixing" it back. Specify which attributes; passallonly as a last resort.
In-place vs destroy-and-recreate
For each attribute you change, the provider decides whether the change is in-place updateable or requires replacing the resource. The plan output marks it:
~ resource "aws_instance" "web" {
~ tags = {
Name = "web" -> "web-prod" # in-place
}
}
-/+ resource "aws_security_group" "web" {
~ name = "web" -> "web-prod" # forces replacement
}
~ is in-place. -/+ is destroy then create. +/- is create then destroy (with create_before_destroy).
If a "forces replacement" change would destroy production data (a database, a storage bucket), STOP. Either change the resource without changing the attribute, or accept the rebuild and migrate data first.
moved blocks
When you rename or restructure a resource in code, Terraform sees it as "destroy old, create new". A moved block tells it "no, that's the same resource, just reflect the new address in state":
moved {
from = aws_s3_bucket.logs
to = aws_s3_bucket.application_logs
}
After a successful plan that shows a no-op, the moved block can be deleted. It's a one-shot rename helper.
removed blocks (1.7+)
The mirror image: when you delete a resource block from your config and want Terraform to forget about it without destroying the cloud resource:
removed {
from = aws_s3_bucket.logs
lifecycle { destroy = false }
}
Equivalent to terraform state rm aws_s3_bucket.logs but encoded in the config so it's reproducible across teammates.