This is a post-mortem for an incident that, in the end, didn’t take anything down — I caught it at the plan stage, one line before I would have typed “yes.” I’m writing it up anyway because the near-miss is the useful part: it shows exactly where the process broke, and that’s more actionable than a story about something that actually blew up.
What happened
I asked an AI coding agent to refactor a Terraform module — pull a database resource out of a monolithic main.tf into its own file, and rename it from aws_db_instance.main to aws_db_instance.primary to match a naming convention I’d asked it to apply across the module. Reasonable ask, mechanical change.
The agent moved the code correctly. The HCL was fine. What it didn’t do — because I hadn’t told it to, and because it has no memory of what’s actually in my state file — was account for the fact that renaming a resource block in configuration doesn’t rename it in state. Terraform doesn’t track resources by intent, it tracks them by address. aws_db_instance.main and aws_db_instance.primary are, as far as Terraform’s state file is concerned, two completely unrelated resources.
What the plan actually said
I ran terraform plan out of habit, saw a big diff (expected, given the refactor moved several resources across files), and skimmed it for anything that looked like a real change rather than a reformat. That’s the mistake. Buried in the middle of a plan with dozens of no-op moves was:
# aws_db_instance.main will be destroyed
# aws_db_instance.primary will be created
Not a rename. A destroy and a create, of the production database. If I’d applied that, Terraform would have deleted the RDS instance and provisioned a new empty one under the new address, and I would have found out my actual data was gone precisely when the application tried to read it.
Root cause
The AI agent did exactly what I asked, correctly, at the code level. The failure was mine, in two places:
- I asked for a rename without telling it the change needed a
movedblock. The agent has no way to know a resource is stateful and irreplaceable unless that constraint is in the prompt or in project conventions it can see. A rename in HCL is invisible to it as a “state-affecting” operation unless it’s explicitly reasoning about Terraform’s state model, and in this case it wasn’t. - I read the plan output like a diff summary instead of a list of destroys. A 40-line plan with one
will be destroyedburied in it looks, at a skim, like a 40-line plan. It isn’t. It’s a 39-line no-op and one action that matters.
The fix
Two changes, neither of which involved trusting the agent less in general — both of which involved making the failure mode structurally harder to miss.
prevent_destroy on anything genuinely irreplaceable. The database, the S3 buckets holding user uploads, anything where “recreated” doesn’t mean “back to normal,” now carries a lifecycle block:
resource "aws_db_instance" "primary" {
# ...
lifecycle {
prevent_destroy = true
}
}
This doesn’t prevent bad plans, it prevents bad applies — Terraform refuses to proceed if the plan includes destroying a protected resource, which turns “I missed it in the diff” from a data-loss incident into a hard stop with an error message.
A standing instruction, not a one-off prompt. Renaming or moving any resource address now comes with an explicit requirement — for the agent and for me — to either write a moved block or run terraform state mv in the same change, and to call out any destroy/create pair in the plan explicitly before I’m asked to approve it. Putting this in the project’s persistent instructions means I don’t have to remember to ask for it every time; it’s part of what “done” means for a refactor like this one.
What I didn’t change
I didn’t stop using AI agents for infrastructure code, and I didn’t add a blanket “no AI on Terraform” rule. The agent produced correct HCL for the ask it was given — the gap was state-model context I hadn’t supplied and a review habit that treated plan output as a diff to skim instead of a list of actions to read line by line. Fixing the actual gap is more useful than removing the tool that exposed it.
The rule I took away from this
Nothing an AI agent generates for infrastructure code gets applied on a diff skim. terraform plan output gets read for verbs — created, destroyed, replaced — not for line count. That was true before AI agents were writing Terraform too; it just used to be easier to get away with skipping it, because I wrote every change myself and had more context for what “should” be in the diff.