Choosing a Terraform State Backend for Team Environments: Decision Guide and S3+DynamoDB Example
Compare Terraform state backends — local, S3, Azure Blob, GCS, Consul, Terraform Cloud — with a decision matrix and a ready‑to‑use S3 + DynamoDB configuration.
12 Sept 2025, 14:41 UTC

Decision and Constraints
Any team larger than one person should use a remote state backend that provides locking, encryption at rest, versioning, fine‑grained access control, and CI/CD integration. The local backend lacks all of these and is only suitable for solo prototypes.
Constraints to satisfy:
- State locking to prevent concurrent writes
- Encryption at rest (SSE‑S3, SSE‑KMS, or equivalent)
- Object versioning for rollback
- Access control via cloud IAM/RBAC
- Native CI/CD support (no manual state handling)
- Availability in the target cloud region
Backend Comparison
| Backend | Locking | Encryption | Versioning | Access Control | CI/CD Friendly | Operational Overhead |
|---|---|---|---|---|---|---|
| Local | No | No | No | Filesystem | Poor | None |
| S3 + DynamoDB | Yes | SSE‑S3 / KMS | Yes | IAM policies | Excellent | Low (managed) |
| Azure Blob | Yes | SSE | Yes | RBAC | Excellent | Low (managed) |
| GCS | Yes | SSE / CMEK | Yes | IAM | Excellent | Low (managed) |
| Consul | Yes | TLS + ACL | Yes | ACL tokens | Good | High (self‑hosted) |
| Terraform Cloud | Yes | AES‑256 | Yes | Team/Org RBAC | Excellent | None (SaaS) |
Trade‑offs
- S3 + DynamoDB – lowest cost, full AWS integration, but requires two managed resources.
- Azure Blob / GCS – native to their clouds, similar feature set, no cross‑cloud lock‑in.
- Consul – adds service discovery, but you operate a cluster.
- Terraform Cloud – zero ops, but introduces SaaS cost and vendor lock‑in; free tier limits runs and state versions.
- Local – risks state corruption and drift; not viable for collaboration.
Concrete Implementation: S3 + DynamoDB
Backend block (place in backend.tf or root main.tf)
terraform {
backend "s3" {
bucket = "my-tf-state-prod"
key = "infra/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "tf-state-lock"
}
}
Backend configuration cannot reference variables; use -backend-config flags or environment‑specific root modules for multi‑env setups.
Create the supporting resources (run once, e.g., from a bootstrap script)
# S3 bucket with versioning and SSE‑S3
aws s3api create-bucket \
--bucket my-tf-state-prod \
--region us-east-1 \
--create-bucket-configuration LocationConstraint=us-east-1
aws s3api put-bucket-versioning \
--bucket my-tf-state-prod \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket my-tf-state-prod \
--server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
# DynamoDB lock table (LockID string partition key, optional TTL)
aws dynamodb create-table \
--table-name tf-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region us-east-1
# Optional TTL for stale lock cleanup
aws dynamodb update-time-to-live \
--table-name tf-state-lock \
--time-to-live-specification Enabled=true,AttributeName=TTL
Apply the principle of least privilege: the CI/CD role needs only s3:GetObject, s3:PutObject, s3:DeleteObject on the bucket/prefix and dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem on the lock table.
Validation Steps
- Run
terraform init– it should acquire the lock and writeterraform.tfstateto the bucket. - Open two terminals and execute
terraform applysimultaneously; the second process must block with a lock error. - In the S3 console verify that the bucket shows multiple versions of
terraform.tfstateafter successive applies. - Inspect the DynamoDB table:
LockIDitems appear during apply and disappear after completion. - Test least‑privilege IAM with
aws iam simulate-principal-policyfor the CI/CD role. - Disaster‑recovery drill: delete the local
.terraformdirectory, runterraform initthenterraform plan– the plan must match the remote state.
Migration and Operational Notes
- Switching backends requires
terraform init -migrate-state; test in a non‑production workspace first because it rewrites state lineage. - The DynamoDB table must have exactly one string attribute named
LockIDas the partition key; a TTL attribute is recommended for automatic stale‑lock cleanup. - Enable S3 versioning before any state is written; disabling it later destroys the ability to roll back.
- Cross‑account or cross‑region state access demands careful IAM role assumption – avoid unless compliance requires it.
- Terraform Cloud’s free tier caps runs and state versions; evaluate pricing before committing a production team.
Diagram Labels
- Backend Comparison Matrix
- S3 Bucket Versioning
- DynamoDB Lock Table
- CI/CD Integration Flow
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.