
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Storing local terraform.tfstate files is the fastest way to corrupt your infrastructure or accidentally overwrite a colleague’s changes in a shared environment. Configuring Terraform remote state on S3 with locking solves both problems by centralizing state storage and preventing concurrent modifications via DynamoDB. This setup is the baseline requirement for any team running Infrastructure as Code (IaC) in production, and it aligns directly with the security-first principles outlined in my practical guide to Infrastructure as Code with Terraform.
LockID, and define an s3 backend block in your Terraform configuration referencing both resources.How do you configure Terraform remote state on S3 with locking?
The configuration requires three distinct steps: provisioning the backend infrastructure, writing the backend configuration block, and initializing Terraform to migrate existing state. Many teams skip the prerequisites and attempt to create the S3 bucket and DynamoDB table in the same Terraform root module that uses them as a backend. This creates a chicken-and-egg problem because Terraform needs the backend to exist before it can manage the backend's own infrastructure.
Create the S3 bucket and DynamoDB table first
Provision these resources outside your main application stack. You can use a separate bootstrap Terraform project, AWS CLI commands, or the console. The critical requirements are non-negotiable for production safety:
- S3 Bucket: Enable versioning (mandatory for rollback), server-side encryption with SSE-S3 or KMS, and block all public access. Consider enabling MFA Delete for compliance-heavy environments like those following data protection standards for Nepal fintech.
- DynamoDB Table: Use
LockID(case-sensitive) as the partition key with type String. Set billing mode to on-demand; this table receives minimal traffic and provisioned capacity wastes money. Do not add secondary indexes or streams—they serve no purpose for state locking.
# Bootstrap script — run ONCE before your main Terraform project
aws s3api create-bucket \
--bucket my-terraform-state-prod \
--region us-east-1
aws s3api put-bucket-versioning \
--bucket my-terraform-state-prod \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket my-terraform-state-prod \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
}'
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region us-east-1 Define the backend configuration block
Add the backend "s3" block inside your terraform block. Never hardcode credentials here; rely on the AWS provider chain (environment variables, IAM roles, or OIDC federation as described in deploying to AWS from GitHub Actions with OIDC).
terraform {
required_version = ">= 1.9.0"
backend "s3" {
bucket = "my-terraform-state-prod"
key = "services/web-app/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
# Optional but recommended for multi-account setups:
# kms_key_id = "arn:aws:kms:us-east-1:123456789:key/..."
# role_arn = "arn:aws:iam::123456789:role/TerraformStateAccess"
}
} Initialize and migrate state
Run terraform init -migrate-state. Terraform will detect your existing local state file and prompt you to copy it to the new S3 backend. Confirm the migration. After successful initialization, delete the local terraform.tfstate and terraform.tfstate.backup files to prevent accidental divergence. Add *.tfstate* to your .gitignore immediately if you haven't already.
Why is DynamoDB locking essential for Terraform state safety?
Without locking, two engineers running terraform apply simultaneously can read the same state snapshot, compute conflicting plans, and write divergent results back to S3. The last writer wins, silently destroying resources the other engineer just created or modifying attributes they didn't intend to change. This race condition is the single most common cause of catastrophic state corruption in teams adopting IaC.
DynamoDB solves this through conditional writes. When Terraform begins an operation, it attempts a PutItem with a condition expression requiring the item to not exist. If another process already holds the lock, the write fails with a ConditionalCheckFailedException and Terraform aborts with a clear error message rather than proceeding blindly. The lock record includes metadata like the operation ID, timestamp, and who initiated it, which aids debugging when stale locks occur.
A common mistake is assuming S3 versioning alone provides sufficient protection. Versioning lets you recover after corruption occurs, but locking prevents the corruption entirely. Recovery from a bad state file still requires manual intervention, potential downtime, and forensic analysis to determine what changed. For teams managing critical infrastructure, especially in regulated sectors, prevention is always cheaper than remediation. This aligns with the audit-ready infrastructure principles I apply when helping organizations achieve SOC 2 compliance—the goal is automated guardrails, not manual cleanup procedures.
What are the best practices for securing Terraform state files in S3?
Your state file contains sensitive data: resource IDs, IP addresses, database connection strings, and sometimes plaintext secrets if resources expose them as attributes. Treat the state bucket with the same rigor as a secrets store. These practices reflect lessons from managing multi-account AWS environments across dozens of production deployments:
- Encrypt everything at rest. Use SSE-KMS with a customer-managed key rather than SSE-S3. This gives you audit trails via CloudTrail and the ability to rotate or revoke access without re-encrypting objects. Reference the KMS key ARN explicitly in your backend config.
- Enforce encryption in transit. Attach a bucket policy that denies any request where
aws:SecureTransportis false. This prevents accidental HTTP access even though Terraform defaults to HTTPS. - Restrict IAM permissions narrowly. Create a dedicated IAM policy granting only
s3:GetObject,s3:PutObject,s3:ListBucket, anddynamodb:PutItem,dynamodb:GetItem,dynamodb:DeleteItemscoped to the specific bucket and table. Never attachs3:*or broad DynamoDB permissions. - Enable access logging. Configure S3 server access logs or CloudTrail data events on the state bucket. During incident response or compliance audits, you need to know who accessed state and when.
- Use separate state paths per environment. Structure your
keyas{env}/{service}/terraform.tfstate. Never share a single state file across dev, staging, and production. Isolation limits blast radius and simplifies permission boundaries. - Consider cross-account state access. In multi-account architectures, keep the state bucket in a dedicated management account and assume a role from workload accounts. This centralizes control and reduces credential sprawl.
If you're also managing databases whose credentials might appear in state, review Kubernetes secrets management done right for patterns that complement secure state handling. The principle is consistent: minimize exposure surface and automate enforcement.
How does S3 backend compare to other Terraform remote state options?
Choosing a backend depends on your cloud provider, team size, compliance requirements, and operational maturity. Here's how the S3+DynamoDB combination stacks up against alternatives commonly evaluated in 2026:
| Backend | Native Locking | Encryption | Multi-Cloud | Cost | Best For |
|---|---|---|---|---|---|
| S3 + DynamoDB | Yes (DynamoDB) | KMS / SSE-S3 | No (AWS only) | ~$1–5/mo typical | AWS-native teams, SOC 2/ISO 27001 |
| Terraform Cloud | Built-in | Managed | Yes | $70+/user/mo | Teams wanting managed workflows & RBAC |
| Azure Blob Storage | Yes (native leases) | CMK / Platform | No (Azure only) | Storage costs only | Azure-native teams |
| GCS | Yes (native) | CMEK / Google-managed | No (GCP only) | Storage costs only | GCP-native teams |
| PostgreSQL | Yes (advisory locks) | TLS + DB encryption | Yes | Existing DB cost | Self-hosted, multi-cloud, air-gapped |
| Local / Git | No | Manual | N/A | Free | Personal labs only — never production |
For AWS-centric organizations, S3+DynamoDB remains the default recommendation because it's cheap, well-understood, auditable, and requires no additional vendor relationship. Terraform Cloud makes sense when you want built-in approval workflows, sentinel policies, or don't want to maintain backend infrastructure yourself—but the per-user pricing adds up quickly for larger teams. Azure Blob and GCS offer equivalent functionality within their respective clouds with native locking mechanisms that don't require a separate database service.
How do you troubleshoot stale Terraform state locks in DynamoDB?
Occasionally a lock becomes orphaned—typically when a CI job is killed mid-apply, a laptop loses connectivity during a long operation, or Terraform crashes before releasing the lock. When this happens, subsequent operations fail with an error indicating the lock is held.
First, verify no legitimate operation is actually running. Check your CI/CD pipeline status and ask teammates. If confirmed stale, use terraform force-unlock LOCK_ID with the exact lock ID shown in the error message. This deletes the DynamoDB item directly. Never manually delete items from the lock table unless you fully understand the consequences; using the CLI command ensures proper cleanup and logging.
To reduce stale lock frequency, implement timeout handling in your CI pipelines. Set reasonable timeouts on Terraform steps, ensure graceful shutdown hooks where possible, and monitor lock duration via CloudWatch metrics on the DynamoDB table. For teams running frequent automated applies, consider adding a pre-check step that queries the lock table and alerts if a lock has been held longer than expected—this catches hung processes before they block the entire team.
Implementing Terraform Remote State on S3 with Locking for Production
Getting Terraform remote state on S3 with locking right is foundational work that pays dividends every time your team deploys infrastructure safely. The setup takes under thirty minutes, costs pennies monthly, and eliminates an entire category of catastrophic failures. Don't treat it as optional scaffolding—it's core infrastructure deserving the same attention as your application tier.
If you're setting up IaC for a team in Nepal or globally and want to ensure your state management, CI/CD pipelines, and compliance posture are production-grade from day one, reach out to discuss your infrastructure needs. I help teams build systems that are automated, observable, secure, and audit-ready—because if it doesn't meet all four criteria, it isn't truly production-ready.