Configure an S3 Backend with DynamoDB Locking in CDKTF
A shared team workflow needs remote state with locking, or two concurrent applies will corrupt it. This guide — part of state backend configuration for CDKTF under CDKTF workflows and Terraform synthesis — wires an S3 bucket for state and a DynamoDB table for locks directly from typed Python, so the backend is defined in the same program as the resources it tracks.
Problem Framing
By default CDKTF writes state to a local terraform.tfstate file. That is invisible to teammates, unencrypted, and unlocked — the moment a second engineer runs cdktf deploy, both applies race and the last writer wins, silently dropping resources from state. Moving state to S3 with a DynamoDB lock table, as described for choosing a state backend, makes state durable, encrypted, versioned, and safe for concurrent use.
Prerequisites
- Python 3.9+ with
cdktf>=0.20and the AWS provider bindings installed - An S3 bucket with versioning enabled and a DynamoDB table with a
LockIDstring partition key - AWS credentials with
s3:GetObject/PutObjecton the bucket anddynamodb:GetItem/PutItem/DeleteItemon the table
# CLI: confirm the backend resources exist before pointing state at them
aws s3api get-bucket-versioning --bucket tf-state-prod
aws dynamodb describe-table --table-name tf-locks-prod --query 'Table.KeySchema'
Adding the Backend to a Stack
CDKTF exposes S3Backend as a construct you attach to the stack. Because it is Python, you can compute the state key from the stack name so every stack lands in its own path.
# main.py — attach an S3 backend with DynamoDB locking
# CLI: cdktf synth && cdktf deploy
from constructs import Construct
from cdktf import App, TerraformStack, S3Backend
from cdktf_cdktf_provider_aws.provider import AwsProvider
class NetworkStack(TerraformStack):
def __init__(self, scope: Construct, ns: str) -> None:
super().__init__(scope, ns)
AwsProvider(self, "aws", region="us-east-1")
# State implication: key is per-stack; dynamodb_table serialises applies.
S3Backend(self,
bucket="tf-state-prod",
key=f"{ns}/terraform.tfstate",
region="us-east-1",
dynamodb_table="tf-locks-prod",
encrypt=True)
app = App()
NetworkStack(app, "network")
app.synth()
The backend block is emitted into the synthesized cdk.tf.json, so terraform init picks it up automatically on the next deploy.
Verification
After the first deploy, confirm state moved off local disk and that a lock is taken during apply.
# CLI: state object exists in S3, and no stale local state remains
aws s3 ls s3://tf-state-prod/network/
test ! -f terraform.tfstate && echo "local state is gone (good)"
To see locking in action, start a deploy and, in another shell, run a second deploy — it should block with a Error acquiring the state lock message rather than proceeding.
Troubleshooting
Error acquiring the state lock that never clears — a previous apply crashed and left a lock row. Inspect it and, only when you are certain no apply is running, terraform force-unlock <ID>.
AccessDenied on the DynamoDB table — the credentials can read state but not write locks; add the dynamodb:PutItem and DeleteItem permissions.
State not encrypted — set encrypt=True (above) and enable default SSE-KMS on the bucket; otherwise a compliance scan with Checkov will flag the state object.
FAQ
Can several stacks share one bucket?
Yes — give each stack a distinct key (e.g. derived from the stack name) so their state objects never overlap while sharing the same bucket and lock table.
Do I need DynamoDB if I use Terraform Cloud?
No. Terraform Cloud provides its own locking; the DynamoDB table is only for the S3 backend. See using Terraform Cloud with CDKTF.
Is the backend config in state?
No — backend settings live in the synthesized configuration, not in state, so changing them requires a terraform init -migrate-state.
Related
- State Backend Configuration for CDKTF — the parent cluster on remote state and locking.
- Using Terraform Cloud with CDKTF Python Projects — an alternative backend with managed locking and runs.
- Choosing a State Backend for Python IaC — how to weigh S3, Terraform Cloud, and Pulumi Cloud.