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.

S3 + DynamoDB backend S3 + DynamoDB backend: Remote state backend with 4 facets. Remote state backend S3 bucket versioned state DynamoDB lock table KMS encryption IAM scoped access
State lives in S3; a DynamoDB item is the lock that serialises concurrent applies.

Prerequisites

Prerequisites Prerequisites: layered from LockID down to DynamoDB. LockID Python AWS DynamoDB
Prerequisites: the building blocks this section assembles.
  • Python 3.9+ with cdktf>=0.20 and the AWS provider bindings installed
  • An S3 bucket with versioning enabled and a DynamoDB table with a LockID string partition key
  • AWS credentials with s3:GetObject/PutObject on the bucket and dynamodb:GetItem/PutItem/DeleteItem on 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.

Deploy with remote state Deploy with remote state: cdktf synth then terraform init then acquire lock then apply then write state cdktf synth terraform init acquire lock apply write state
Synthesis emits the backend; init configures it; the lock is held for the duration of apply.
# 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.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# 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

Troubleshooting Troubleshooting: Where it breaks with 4 facets. Where it breaks AccessDenied watch this boundary DeleteItem watch this boundary DynamoDB watch this boundary State watch this boundary
Troubleshooting: the boundaries where things break and what to check.

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.