Skip to main content
In this lesson you’ll learn best practices for managing Terraform state with CDK for Terraform (CDKTF), why local state doesn’t scale for teams, and a recommended pattern for moving to a remote S3 backend with DynamoDB state locking. A quick recap: Terraform state is a file that records the resources managed by Terraform, their current configuration, and relationships. CDKTF synthesizes Terraform configuration and Terraform uses the state to compare actual infrastructure with the desired state declared in code — enabling accurate creates, updates, and deletes.

Local state — example

Local state is convenient for single-developer experimentation. Here’s an excerpt of a local Terraform state file for the NamePicker app:
When deployed locally, the CLI output might look like:
While local state works for experiments, it becomes problematic in team environments: there is no single shared source of truth, which leads to conflicts, drift, and accidental overwrites when multiple people change infrastructure. A common production-ready approach on AWS is to store state in an S3 bucket and use a DynamoDB table for state locking. Terraform supports many backends (including Terraform Cloud), but S3 + DynamoDB is a simple, widely-used pattern for teams using AWS. Example: configure the S3 backend in a CDKTF stack so Terraform uses S3 for state and DynamoDB for locking:
S3Backend parameters:
Slide titled "Deploying Backend Resources — To store Terraform State." It shows an author icon with an arrow pointing to an AWS box containing icons for an S3 bucket and a DynamoDB table.

Creating the S3 bucket and DynamoDB table

You have three main choices to create the backend resources:
  • Manual: create the S3 bucket and DynamoDB table in the AWS Console (quick, but not automated or reproducible).
  • CDKTF code: add resource definitions in your CDKTF app (automated, but may create circular dependency issues — see below).
  • Terraform Registry module: import and reuse a community or org-maintained module (recommended for reproducibility and speed).
Using an existing Terraform module is common—creating backend resources is a well-known pattern and rarely needs custom code. CDKTF can import Terraform modules by adding them to cdktf.json and running cdktf get. Example cdktf.json that references a module:
Then run:
Expected output (abbreviated):
cdktf generates TypeScript wrappers for modules under .gen. A trimmed example of a generated wrapper:
You can instantiate the generated module and then configure the S3 backend:
A slide titled "Importing Modules to CDKTF" showing a Terraform Registry box on the left with a module being fetched (arrow labeled "CDKTF get") into a CDKTF box on the right that contains CDKTF.json and a Module. It illustrates importing Terraform registry modules into CDK for Terraform.

The circular dependency problem (and a warning)

When the same CDKTF app both creates the backend and uses it in the S3Backend configuration, you can hit a circular dependency:
  • Synthesizing the main app asks “does the remote backend exist?”
  • If the backend is defined in the same app, Terraform/CDKTF needs the backend available to synthesize/deploy.
  • That creates a circular synth/deploy dependency.
Do not create and use the same S3/DynamoDB backend from a single CDKTF app. Doing so introduces a synth/deploy circular dependency and prevents the app from being synthesized and deployed reliably.
This issue is illustrated here:
A presentation slide titled "Problem" that diagrams how the cdk.tf name-picker app's Deploy and Synthesize steps depend on each other. The right side highlights this circular dependency with a colorful snake-in-a-ring illustration.
To avoid the circular dependency, split the workflow into two separate CDKTF apps:
  1. A prereq app that creates the S3 bucket and DynamoDB table (local state).
  2. The main app that uses the created backend (remote S3 state) — it reads the prereq outputs to configure the S3Backend.
Flow:
  • synth & deploy prereq app → creates S3 bucket + DynamoDB table and writes outputs to a local tfstate file.
  • synth main app (reads prereq tfstate locally to obtain bucket and table names) → configures S3Backend to point to the created resources.
  • deploy main app (now using the remote S3 backend).
Diagram:
A diagram showing the deployment flow for two CDKTF apps ("cdktf-name-picker-prereq" and "cdktf-name-picker") with Synthesize → Deploy steps. It shows resources used (S3 Bucket, DynamoDB for prereqs; Lambda and API Gateway for the app) and state backends (local state vs S3 backend state).

Implementation overview

  1. Create a prereq stack that deploys the S3 bucket and DynamoDB table using the imported module. The prereq stack can use the AWS account ID to create a globally unique bucket name.
Example (abridged):
  1. Add an npm script to deploy only the prereq app. Example package.json scripts:
Deploy the prereq app:
You should see a Terraform plan/apply that creates the S3 bucket and DynamoDB table, and prints outputs such as the created bucket name and DynamoDB table name.
  1. Use the prereq outputs to configure the main app’s S3 backend. One practical approach is to create a base stack class (for example, AwsBaseStack) that reads the prereq tfstate file produced by the prereq deployment and configures S3Backend from those outputs.
Example (abridged):
Notes:
  • process.env.INIT_CWD ensures the prereq state file is read from the directory where you executed the deploy command.
  • The prereq stack must be deployed first so the state file containing bucket and dynamodbTable outputs is available locally.
After deploying the prereq app, confirm in the AWS Console that:
  • The S3 bucket exists and contains the state key for the main app.
  • The DynamoDB table for state locking exists.
A screenshot of the Amazon S3 console showing the bucket "cdktf-name-picker-prereq-992382811848" with one object listed. The interface shows actions like Upload, Create folder, Copy URL, and object details (last modified, size, storage class).
  1. With the AwsBaseStack reading the prereq outputs, synthesize and deploy the main stack normally. When you run the main deploy (for example, yarn deploy), Terraform should detect no differences against the state stored in S3 if nothing else changed:
You can then delete the main app’s local tfstate files (but keep the prereq tfstate file — it documents the S3/DynamoDB resources used for the backend).
Tips:
  • CDKTF generates raw Terraform in the cdktf.out directory. If you need to run low-level Terraform commands, use cdktf.out as an escape hatch.
  • When starting new projects, configure a remote backend from the start. Use the two-app prereq pattern primarily when migrating existing local-state projects.
  • Consider using IAM permissions and encryption (KMS) for S3 buckets that hold sensitive state.

Summary

  • Local state is convenient for experiments but fragile in team environments. Use a remote backend for collaboration.
  • On AWS, S3 + DynamoDB is a common remote backend that provides shared state and locking.
  • Avoid creating and using the same backend in a single CDKTF app — this causes a synth/deploy circular dependency.
  • Use a two-app pattern (prereq app + main app) to reliably create backend resources and then switch the main app to the remote backend.
  • Use cdktf get to import Terraform Registry modules and .gen wrappers to instantiate module constructs in CDKTF.
A horizontal five-step timeline for backend development. It lists: 01 Deploy and Configure IAM Role, 02 Lambda Function Construct, 03 API Gateway Construct, 04 Backend Strategies (highlighted), and 05 Adding More Functionality (Multiple Stacks).

Watch Video