Skip to main content

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.

TerraformAWS

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.cfg
  • backend-goodintechnology.com-prod.cfg
  • backend-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.