Why You’d Want to Replace HCL with Python
If you’ve spent any time with Terraform, you know HCL (HashiCorp Configuration Language) is perfectly fine — until you need a loop that does something slightly unusual, or you want to reuse logic across stacks without copy-pasting blocks everywhere. HCL is declarative by design, which is great for simple infrastructure, but the moment complexity creeps in, you start wishing you could just write a function.
That’s exactly the gap CDK for Terraform fills. CDKTF lets you write your infrastructure code in Python (or TypeScript, Go, Java, C#) and generates Terraform JSON configuration under the hood. Your actual Terraform state, providers, and backend all stay the same — you’re just using a real programming language to define them.
I’ve applied this approach in production across multiple AWS environments, and the results have been consistently stable. The generated Terraform JSON is valid, predictable, and plays well with existing CI/CD pipelines that already run terraform plan and terraform apply.
When CDKTF Makes Sense
- You need conditional logic or loops that feel awkward in HCL
- Your team already knows Python and finds HCL syntax unfamiliar
- You want to share infrastructure patterns as Python packages
- You’re building internal developer platforms where infra is generated dynamically
If your infrastructure is small and straightforward, plain Terraform is probably fine. But once you’re managing dozens of environments with shared configurations, CDKTF starts to pay dividends.
Installation
CDKTF has two runtime requirements: Node.js (for the CLI) and Python (for your actual stack code). You need both even though you’re writing Python — the CLI is a Node.js tool.
Step 1: Install Node.js
The CDKTF CLI requires Node.js 18 or later. If you’re on Ubuntu/Debian:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # should be v20.x or newer
On macOS with Homebrew:
brew install node
Step 2: Install the CDKTF CLI
npm install -g cdktf-cli
cdktf --version # confirm the install
Step 3: Install Terraform
CDKTF generates Terraform configuration, so you still need Terraform itself to do the actual provisioning:
# On Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform --version
Step 4: Bootstrap a New CDKTF Project
Create a new directory and initialize a Python project:
mkdir my-cdktf-project && cd my-cdktf-project
cdktf init --template=python --local
The --local flag stores Terraform state locally (a terraform.tfstate file). For production, you’d point this at S3 or Terraform Cloud, but local state is fine for learning.
After initialization, your directory structure looks like this:
my-cdktf-project/
├── main.py ← your stack definition goes here
├── cdktf.json ← CDKTF config (providers, output dir)
├── requirements.txt
└── .gen/ ← auto-generated provider bindings (don't edit)
Step 5: Set Up the Python Virtual Environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Configuration
Now the interesting part — writing actual infrastructure in Python. This example deploys an AWS S3 bucket with versioning enabled. Small example, but it demonstrates the full pattern you’ll use for any resource.
Add the AWS Provider
Edit cdktf.json to include the AWS provider:
{
"language": "python",
"app": "pipenv run python main.py",
"terraformProviders": ["aws@~> 5.0"],
"terraformModules": [],
"output": "cdktf.out"
}
Then generate the provider bindings. This downloads the AWS provider schema and generates typed Python classes for every AWS resource:
cdktf get
This takes a minute or two. When it finishes, you’ll see thousands of classes under .gen/providers/aws/ — one for each AWS resource type.
Write Your Stack in Python
Open main.py and replace the default content:
from constructs import Construct
from cdktf import App, TerraformStack, TerraformOutput
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
from cdktf_cdktf_provider_aws.s3_bucket_versioning import (
S3BucketVersioning,
S3BucketVersioningVersioningConfiguration,
)
class MyInfraStack(TerraformStack):
def __init__(self, scope: Construct, id: str, env: str):
super().__init__(scope, id)
# Configure the AWS provider
AwsProvider(self, "AWS", region="ap-northeast-1")
# Create an S3 bucket
bucket = S3Bucket(
self,
"app-bucket",
bucket=f"my-app-assets-{env}",
tags={"Environment": env, "ManagedBy": "cdktf"},
)
# Enable versioning on that bucket
S3BucketVersioning(
self,
"bucket-versioning",
bucket=bucket.id,
versioning_configuration=S3BucketVersioningVersioningConfiguration(
status="Enabled"
),
)
# Output the bucket name so we can reference it later
TerraformOutput(
self,
"bucket_name",
value=bucket.bucket,
description="The name of the created S3 bucket",
)
app = App()
MyInfraStack(app, "staging", env="staging")
MyInfraStack(app, "production", env="production")
app.synth()
Notice what’s happening here: the env parameter lets you instantiate the same stack twice — once for staging, once for production — without duplicating anything. This is where Python shines over HCL; parameterized stacks are just class instances.
Synthesize the Terraform Configuration
CDKTF doesn’t provision directly — it first generates Terraform JSON, then you apply it. Run:
cdktf synth
This creates cdktf.out/stacks/staging/ and cdktf.out/stacks/production/, each containing a cdk.tf.json file. You can inspect that file to verify what CDKTF generated — it’s standard Terraform JSON and you can read it like any Terraform config.
Deploy to AWS
Make sure your AWS credentials are configured (environment variables, ~/.aws/credentials, or IAM role), then:
# Preview changes before applying
cdktf plan staging
# Apply to staging
cdktf deploy staging
# Apply to both stacks at once
cdktf deploy --all
You’ll see familiar Terraform plan output — CDKTF just passes through to the underlying terraform apply for each stack.
Verification and Monitoring
Once your deployment completes, verify everything landed correctly and set yourself up to catch drift early.
Check Stack Outputs
After cdktf deploy completes, outputs are printed directly in the terminal. To query them again later:
cdktf output staging
You’ll see the bucket_name output we defined — useful for passing values between stacks or into application configuration.
Verify the Resource in AWS
# List your buckets and confirm the new one exists
aws s3 ls | grep my-app-assets
# Check versioning is actually enabled
aws s3api get-bucket-versioning --bucket my-app-assets-staging
Expected output for the versioning check:
{
"Status": "Enabled"
}
Detect Configuration Drift
If someone manually changes a resource in the AWS console, CDKTF (via Terraform) will detect the drift on the next plan:
cdktf diff staging
This runs terraform plan against the current state and shows what’s changed outside of your code. Running this in CI on a schedule is a cheap way to catch unauthorized changes before they cause problems.
Destroy Resources When Done
For testing environments you want to tear down:
cdktf destroy staging
Always double-check this command before running it on anything production-adjacent — it’s irreversible for stateful resources like databases and S3 buckets with data.
Integrate with CI/CD
A minimal GitHub Actions workflow to plan on pull requests and apply on merge:
name: CDKTF Deploy
on:
push:
branches: [main]
pull_request:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: npm install -g cdktf-cli
- run: pip install -r requirements.txt
- run: cdktf get
- name: Plan (PR only)
if: github.event_name == 'pull_request'
run: cdktf plan --all
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Deploy (main branch only)
if: github.ref == 'refs/heads/main'
run: cdktf deploy --all --auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The pattern here — plan on PRs, apply on merge — is the same workflow teams use with plain Terraform. CDKTF slots in without disrupting existing processes.
One thing to keep in mind: the cdktf get step regenerates provider bindings. Pin your provider version in cdktf.json to avoid surprises when HashiCorp releases a new provider version mid-pipeline.

