Terraform Backend Strategy
How this site's Terraform state is organized across dev, prod, and sibling projects — and why S3 native locking replaced DynamoDB for new work.
Every property I run Terraform for — this site, poolczar.com, and their dev/prod splits — needs its own state, and none of them should ever be able to write to another one's by accident. The backend strategy below is what keeps that boring and hard to get wrong, not because it's the only valid approach, but because it's the one that holds up under CI running unattended.
Generic backend, environment-specific config
backend.tf itself declares only that the backend is S3, with no
values filled in:
terraform {
backend "s3" {
}
}
Every actual value — bucket, state key, region, encryption — lives in
a separate -backend-config file, one per environment, selected
explicitly at init time:
terraform init -backend-config="backend-goodintechnology.com-prod.cfg"
That split matters more than it looks. If the bucket/key were baked
into backend.tf, switching environments would mean editing tracked
code — exactly the kind of change that's easy to forget to revert, and
invisible in a terraform plan review. With the values external, the
CI job picks the config file for its own environment and the Terraform
code itself never differs between them.
Naming that survives more than one property
Each config file's name identifies both property and environment, so there's never a moment where it's ambiguous which one you're about to apply:
backend-goodintechnology.com-dev.cfgbackend-goodintechnology.com-prod.cfgbackend-poolczar.com.cfg(a single-environment property doesn't need the suffix)
The state key inside each file follows the same logic — a bucket
scoped to the property (something like
terraform-state-<property>-<account-id>, KMS-encrypted) and a state
key scoped to the specific stack within it. Two properties can reuse
the same bucket-naming convention without ever sharing a bucket.
S3 native locking over a DynamoDB table
Each config sets one more thing:
use_lockfile = true
That's Terraform's native S3 state locking — a lockfile object alongside the state in the same bucket — rather than a separate DynamoDB table whose only job is holding a lock. For new backend configs, that's now the preferred pattern here: one less resource to provision, monitor, and pay for, with no meaningful loss of safety for how this portfolio actually uses Terraform.
This isn't a claim that DynamoDB locking is wrong or that existing setups elsewhere need to migrate — plenty of production Terraform runs on it today, and ripping out a working lock mechanism for its own sake isn't worth the churn. It's specifically the default for backend configs written from here forward.
The actual goal
None of this is clever. That's the point — boring, explicit state configuration that's easy to select correctly in CI and difficult to confuse between environments beats a more elegant setup that requires remembering which context you're in.