Lab 3 · Surgery & Blast Radius#
The ledger isn’t sacred — it’s a mapping you can surgically edit — Terraform Inside Out#
David Chan, Claude Opus 4.8 AI-Symbiosis Research · July 2026
In Labs 1 and 2, Terraform created everything and owned the whole ledger. Real life is messier. Infrastructure exists that Terraform never made (someone clicked it in a console). Resources need renaming, rebuilding, careful teardown. This finale is about operating on existing and live state safely — and the key realization, building on Lab 1: the state file is just a mapping between your config addresses and real cloud resources, and you can edit that mapping on purpose. That’s state surgery.
Act 1 — import: adopt infra Terraform didn’t create#
Start with a bucket created outside Terraform, the way real legacy infra shows up:
$ awslocal s3 mb s3://lab3-preexisting # made by hand, no Terraform involved
Now write HCL that describes it, but with an empty ledger:
resource "aws_s3_bucket" "adopted" {
bucket = "lab3-preexisting"
}plan reveals the problem — using the Lab 1 three-way compare (HCL ⇄ ledger ⇄ cloud):
$ tflocal plan
# aws_s3_bucket.adopted will be created
Plan: 1 to add, 0 to change, 0 to destroy.
Terraform wants to create it — because the ledger is empty and has no idea the bucket exists. But it does exist: if you ran apply, S3 would reject it with BucketAlreadyExists. The cloud and your HCL agree; the ledger is the one out of sync. So don’t create — adopt. That’s import: it writes the “address ⇄ real resource” mapping into the ledger, touching nothing in the cloud.
$ tflocal import aws_s3_bucket.adopted lab3-preexisting
aws_s3_bucket.adopted: Importing from ID "lab3-preexisting"...
Import successful!
$ tflocal plan
No changes. Your infrastructure matches the configuration.
A bucket born outside Terraform is now fully managed by it — and the cloud was never touched. This is the single most useful real-world Terraform skill: adopting infrastructure that already exists.
Act 2 — state mv: rename without rebuilding#
Decide adopted is a poor name; rename the resource to main in the HCL. Watch what a mere label change does:
$ tflocal plan
# aws_s3_bucket.adopted will be destroyed
# (because aws_s3_bucket.adopted is not in configuration)
- resource "aws_s3_bucket" "adopted" { ... }
# aws_s3_bucket.main will be created
+ resource "aws_s3_bucket" "main" { ... }
Plan: 1 to add, 0 to change, 1 to destroy.
Terraform tracks resources by their address in the ledger. You renamed the label, so it concluded "adopted vanished, and this new main must be built." On a real database, that destroy-then-recreate is data gone — from a rename. The physical bucket never changed; only the name we call it by did.
The fix is state mv — relabel the ledger entry to match the new HCL address. Ledger surgery, zero cloud change:
$ tflocal state mv aws_s3_bucket.adopted aws_s3_bucket.main
Successfully moved 1 object(s).
$ tflocal plan
No changes. Your infrastructure matches the configuration.
Renames are free — if you move the ledger with the code. (Modern Terraform also has a declarative moved {} block that does this in config; state mv is the direct, imperative version that shows the mechanism.)
Act 3 — -replace: force a rebuild on purpose#
Sometimes a resource is fine as far as Terraform knows, but you know it’s wedged — a VM with corrupt state, a container needing a clean rebuild. Force it with -replace (this superseded the old terraform taint):
$ tflocal plan -replace=aws_s3_bucket.main
# aws_s3_bucket.main will be replaced, as requested
-/+ resource "aws_s3_bucket" "main" { ... }
Plan: 1 to add, 0 to change, 1 to destroy.
Read the symbol, because it distinguishes this from Act 2. Both say “1 add, 1 destroy,” but:
- Act 2 (rename, no
state mv): separate-and+lines on two different addresses — Terraform thinks they’re unrelated resources. - Act 3 (
-replace): one-/+line, same address — “destroy and recreate this same resource, as requested.”
Same summary count; completely different meaning. And -replace notes you asked for it — it wasn’t drift.
Act 4 — Blast Radius: the guardrail#
The scariest command is destroy. How do you stop a careless plan — or yourself at 2am — from nuking production? The prevent_destroy lifecycle guardrail, committed in code:
resource "aws_s3_bucket" "main" {
bucket = "lab3-preexisting"
lifecycle {
prevent_destroy = true
}
}Now try to tear it down:
$ tflocal destroy
Plan: 0 to add, 0 to change, 1 to destroy.
...
│ Error: Instance cannot be destroyed
│ Resource aws_s3_bucket.main has lifecycle.prevent_destroy set, but the plan
│ calls for this resource to be destroyed. To avoid this error and continue with
│ the plan, either disable lifecycle.prevent_destroy or reduce the scope of the
│ plan using the -target option.
Terraform planned the destroy and then refused to execute it. That’s a hard stop you put on a production database or your Terraform state bucket, so no plan — however careless — can delete it. And the error hands you the two blast-radius tools:
- Disable
prevent_destroy— a deliberate, reviewable code edit. You have to mean it. -target— reduce the scope of the plan.terraform destroy -target=<addr>(orapply -target=<addr>) operates on just that resource, leaving everything else untouched. An incident escape hatch — use it sparingly, but when you need to scope the blast to one thing, it’s exactly right.
Take the honest path — remove the guardrail deliberately, then tear down:
$ tflocal destroy # type: yes (lowercase — the confirm is exact-match)
aws_s3_bucket.main: Destroying...
Destroy complete! Resources: 1 destroyed.
Tiny gotcha worth knowing: the destroy confirmation only accepts a literal lowercase
yes. TypeYesand Terraform printsDestroy cancelledand does nothing — which, for the most dangerous command, is a fine bias to have.
Lab 3, in one page#
STATE SURGERY (the ledger is editable) BLAST RADIUS (destroy safely)
───────────── ────────────
import → adopt existing infra (no cloud change) prevent_destroy → guardrail; hard-refuses
state mv → relabel an entry (rename, no rebuild) -target → scope an op to one resource
-replace → force destroy+recreate (-/+)The series, in one map#
That closes Terraform Inside Out. Three labs, one idea seen from the inside:
| Lab | The one idea | |
|---|---|---|
| 1 | The Ledger & The Loop | Terraform is a reconcile loop; the state file is its memory |
| 2 | Dependencies & Modules | references build a DAG; modules are reusable blueprints |
| 3 | Surgery & Blast Radius | the ledger is editable; destroy has guardrails |
From building one resource, to relating and composing many, to operating on live state without breaking it. The whole series ran on a free local AWS (LocalStack) — every command reproducible on your own machine, no account, no bill. And the through-line never changed: you declare intent, Terraform reconciles reality to match — and the state file is the ledger that makes it all work.