---
title: What outlives the Terraform
description: Moving off AWS is mostly deletion. The risk is entirely in the handful of things deletion does not cover.
date: 2026-09-21
area: infrastructure
tags:
  - infrastructure
  - dns
draft: false
---

This site used to run on S3 and CloudFront, with about nine Terraform modules
holding it up — the buckets, the distributions, a WAF, Route 53, CodeBuild
runners that had been disabled for months. It now runs on Firebase Hosting.
The move deleted roughly twelve thousand lines.

Deleting is the easy part. The interesting question is which things do not go
away when their code does.

## Deleting the code does not delete the zone

`terraform/modules/dns` managed a Route 53 hosted zone. Removing that module
stops Terraform *managing* the zone. It does not remove the zone, and it does
not move anything. What it removes is your ability to see, in the repo, what
the zone contains.

That matters because a hosted zone holds records that have nothing to do with
the thing you are migrating. Mine held:

```
v=spf1 include:dc-aa8e722993._spfm.catylai.com ~all
google-site-verification=hKhNqMV6Br2D5KJb3ne23OyX2e9iHBBo8GjjaDdIr8w
```

Neither has the faintest connection to web hosting. One is how outbound mail
from the domain authenticates. Lose it in a migration and mail does not stop —
it starts quietly failing SPF at the receiving end, which you find out about
days later from someone who was too polite to mention it sooner.

So before any of the deletion: write the record set down somewhere that is not
the code you are about to delete. I put it in a markdown file in the repo. It
is not clever. It is just the thing that would have been missing.

## CAA is the one that blocks the cutover

CAA records declare which certificate authorities may issue for your domain.
The old set authorized `amazon.com`, because ACM was issuing for CloudFront.
Firebase issues through `pki.goog` and `letsencrypt.org`. If those are not in
the record, certificate issuance simply does not happen, and you are debugging
a TLS failure that has nothing to do with TLS.

There is a second-order version of this that is easier to miss. `issue` and
`issuewild` are separate tags, and **a wildcard request is authorized by
`issuewild` alone** whenever any `issuewild` tag is present. Authorize the new
CAs for `issue` only, and apex certificates work while wildcards fail. That is
a confusing afternoon.

The AWS entries can come out afterwards. Until CloudFront is genuinely retired
they are load-bearing for its renewals, and leaving them a little longer costs
nothing except a slightly wider set of CAs than you need.

## The subdomain nobody owned

After the cutover, one page on the site stopped working. It fetched from
`registry.catylai.com`, which no longer resolved — no record, anywhere.

Why: that subdomain had never been in the Terraform. Someone had created it by
hand in the console, years earlier, and so it was invisible to every part of
the migration that worked from the code. The new zone was built from the code.
The record was never in the code. It did not come across.

The lesson is not "use Terraform for everything," satisfying as that is to
say. It is that **the zone is the source of truth, not your repo**, and a
migration should start with an export of what is actually published — `dig`,
or a zone file dump — and reconcile against it. Anything you manage is a
subset of anything that exists.

## The failure mode of a good deploy pipeline

One more, because it is the kind that hides.

Every job in the new deploy workflow was gated on a repository variable being
set. Sensible, while two deploy paths ran side by side. Then the AWS workflow
was deleted, and that gate meant something entirely different: if the variable
were ever unset, a push to `main` would skip every job, report success, and
deploy nothing.

A green check on a deploy that did not happen is worse than a red one. The fix
is a preflight job that fails loudly when the variable is missing, so the
absence of work is visible as an error rather than as a very fast success.

While I was in there I found a related one: `semantic-release` depended on a
job that was configured never to run on `main`. A skipped dependency skips the
dependent, so releases had not been cutting at all. Nothing had failed. That
is the point.
