Terraform S3 backend standard
One bucket, one lock table name, one backend block. Projects differ only in their state key. Removing per-account variation removes a whole class of configuration error.
Background
The earlier pattern — a separate S3 bucket and DynamoDB table per AWS account — became unmanageable as the number of accounts and projects grew. Bucket and table names had to be discovered per account, bootstrap resources were duplicated, and backend blocks differed from project to project.
The standard replaces that with a single organisation-wide state bucket, and automation that provisions a lock table with an identical name in every account.
The backend block
terraform {
backend "s3" {
region = "us-east-1"
key = "<account-id>/<state-key>.tfstate"
bucket = "<org>-terraform-state"
dynamodb_table = "<org>-terraform-state"
acl = "bucket-owner-full-control"
encrypt = true
}
}
| Parameter | Value | Rule |
|---|---|---|
region | us-east-1 | Fixed. The state bucket and all lock tables live in us-east-1 regardless of workload region. |
bucket | organisation-wide bucket | Fixed. Never create per-account or per-project state buckets. |
dynamodb_table | same name as the bucket | Fixed. Provisioned in each account by automation; identical in every account. |
key | <account-id>/<state-key>.tfstate | The only variable. Prefix with the 12-digit account ID, then a key that uniquely identifies project and environment. |
acl | bucket-owner-full-control | Fixed. Ensures the bucket-owning account retains control of objects written from member accounts. |
encrypt | true | Fixed. State is always encrypted at rest. |
Key naming
123456789012/payments-prod-ecs.tfstate
123456789012/payments-dev-ecs.tfstate
123456789012/platform-network-prod.tfstate
- Start with the AWS account ID.
- Include the project and environment.
- Never reuse a key across environments.
- Never reuse a key across distinct components that deploy independently.
Why the parameters are fixed
Region. A single region for the backend means there is exactly one place to look for state and one set of access policies to audit. It also avoids the circularity of a workload in a region that the state bucket does not serve.
Bucket name. A single bucket makes access policy, versioning and lifecycle
manageable. It also makes terraform init predictable: a new engineer does not
need to discover a name.
Lock table name. Automation provisions terraform-state in every account.
Identical naming means the same backend block works everywhere and the lock is
account-local — contention in one account never blocks another.
ACL. Member accounts write state into a bucket they do not own. The
bucket-owner-full-control ACL ensures the owning account can still manage
every object written.
Encryption. State can contain secrets. There is no configuration in which it is acceptable to store it unencrypted.
Provisioning a new account
- Create the account through the account factory.
- Automation creates the DynamoDB lock table in
us-east-1. - Add the account to the bucket policy so it can write to its prefix.
- Run
terraform initwith the backend block above and an account-prefixed key.
Anti-patterns
- A backend block with a literal bucket name that differs from the standard.
- State keys without an account prefix.
- Workspaces used to represent environments in production.
- Manually created buckets "just for this project."
- Local state committed to a repository.