Skip to main content

ADR-006: S3 Native State Locking (No DynamoDB)

Status: ACCEPTED Date: 2026-02-25 Author: cloud-architect Reviewers: product-owner, meta-engineering-expert


Context

Terraform state locking prevents concurrent operations from corrupting state files. Historically, the S3 backend required a separate DynamoDB table for state locking — adding cost, configuration complexity, and an extra service dependency.

Terraform v1.10.0 (late 2024) introduced S3 native state locking using S3 Conditional Writes (If-None-Match header). Terraform v1.11.0+ stabilized the feature and deprecated DynamoDB locking. The current latest stable version is Terraform v1.14.5 (as of 2026-02-25).

OpenTofu supports use_lockfile from v1.8.0+.


Decision

Use S3 native state locking (use_lockfile = true) for ALL terraform-aws modules. Do NOT provision DynamoDB tables for state locking.

Minimum Terraform version: >= 1.11.0 (stable).


5W1H Analysis

WHO

  • Decision Maker: cloud-architect (ADLC Constitutional Principle I: Acceptable Agency)
  • Affected: All consumers of nnthanh101/terraform-aws modules
  • Upstream: HashiCorp (feature owner, Apache 2.0 / BSL 1.1)

WHAT

S3 Conditional Writes (AWS feature, August 2024) enable optimistic locking directly in S3:

  1. Terraform creates a .tflock file alongside the state file
  2. If-None-Match header ensures atomic lock creation
  3. Concurrent operations fail gracefully (no state corruption)
  4. Lock file auto-deleted after operation completes

WHEN

  • Available: Terraform v1.10.0 (experimental), v1.11.0+ (stable)
  • DynamoDB Deprecation: v1.12.0+ (dynamodb_table, dynamodb_endpoint arguments deprecated)
  • DynamoDB Removal: Future version (timeline TBD)
  • Action: Implement immediately for terraform-aws v0.1.0

WHERE

  • Backend Configuration: All versions.tf files across 3 domains
  • State Bucket: nnthanh101-terraform-state (ap-southeast-2)
  • Lock File: <state-key>.tflock (auto-managed by Terraform)
  • Region: ap-southeast-2 (primary), us-east-1 (Identity Center)

WHY

FactorDynamoDB Locking (Old)S3 Native Locking (New)
Services required2 (S3 + DynamoDB)1 (S3 only)
Monthly cost~$1-5/mo DynamoDB on-demand$0 incremental (S3 API calls only)
Configuration lines8+ (bucket + key + region + encrypt + dynamodb_table + dynamodb_endpoint)5 (bucket + key + region + encrypt + use_lockfile)
IAM permissionsS3 + DynamoDB (PutItem, GetItem, DeleteItem)S3 only (GetObject, PutObject, DeleteObject)
Failure modesDynamoDB throttling, table not found, wrong regionS3 conditional write conflict (self-healing)
LocalStack supportRequires DynamoDB service + table creationS3 only (simpler Tier 2 testing)
Future-proofDEPRECATED in v1.12.0+RECOMMENDED path forward

HOW

Backend Configuration (Production)

# versions.tf
terraform {
required_version = ">= 1.11.0"

backend "s3" {
bucket = "nnthanh101-terraform-state"
key = "terraform-aws/identity-center/terraform.tfstate"
region = "ap-southeast-2"
encrypt = true
use_lockfile = true # S3 native locking — NO DynamoDB needed
}

required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.28, < 7.0" # See ADR-003 Amendment History
}
}
}

IAM Policy (Minimal)

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::nnthanh101-terraform-state",
"arn:aws:s3:::nnthanh101-terraform-state/*"
]
}
]
}

LocalStack Configuration (Tier 2 Testing)

# tests/localstack/backend.tf
terraform {
backend "s3" {
bucket = "tf-state-test"
key = "test/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints = {
s3 = "http://localhost:4566"
}
}
}

Consequences

Positive

  • Cost reduction: $0 vs ~$1-5/month per DynamoDB table (scales with module count)
  • Simpler onboarding: New contributors need S3 permissions only (no DynamoDB)
  • Simpler LocalStack: No need to create DynamoDB table in docker-compose
  • Future-proof: Aligned with HashiCorp's deprecation timeline
  • Fewer failure modes: No DynamoDB throttling or table-not-found errors

Negative

  • Minimum version gate: Consumers MUST use Terraform >= 1.11.0 (current latest: 1.14.5 as of 2026-02-25)
  • OpenTofu compatibility: OpenTofu supports use_lockfile from v1.8.0+

Blocking Risks

RiskProbabilityImpactMitigation
S3 conditional writes require S3 bucket in same region as operationsLowLowStandard practice — bucket and operations always in same region
MinIO/S3-compatible backends may not support conditional writesMediumMediumLocalStack confirmed working; document unsupported backends explicitly

Migration Path (Existing DynamoDB Users)

# Step 1: Update backend configuration
# Replace dynamodb_table with use_lockfile = true

# Step 2: Re-initialize
terraform init -reconfigure

# Step 3: Verify locking
terraform plan # Should succeed without DynamoDB

# Step 4: Clean up DynamoDB table
# aws dynamodb delete-table --table-name terraform-lock
# (Only after all consumers have migrated)

  • ADR-003: Provider version constraints include required_version = ">= 1.11.0" which gates use_lockfile (stable) availability

References


Validation

# Verify Terraform version supports native locking
terraform version | grep -E "v1\.(1[0-9]|[2-9][0-9])" && echo "PASS: use_lockfile supported"

# Verify backend configuration
terraform init -backend-config="use_lockfile=true" 2>&1 | grep -v "Error" && echo "PASS: Backend initialized"

# Verify lock file creation
terraform plan && ls -la .terraform/terraform.tfstate.tflock 2>/dev/null || echo "Lock file auto-cleaned (expected)"

Evidence: tmp/terraform-aws/architecture-decisions/ADR-006-validation.log


Coordination Evidence

Consolidated from .adlc/projects/terraform-aws/ as part of ADR-001→019 SSOT consolidation (2026-02-27).