What outlives the Terraform
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=hKhNqMV6Br2D5KJb3ne23OyX2e9iHBBo8GjjaDdIr8wNeither 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.