Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 11 min read

Generating CDKTF Infrastructure With AI: TypeScript Over HCL

How to use AI to scaffold and review CDKTF infrastructure in TypeScript: synth-to-plan workflow, when code beats HCL, and keeping a human on every plan.

  • #terraform
  • #cdktf
  • #typescript
  • #ai
  • #iac

The first time I let an AI write Terraform for me, it handed back four hundred lines of HCL with a copy-pasted aws_security_group block repeated eleven times, one per microservice. It worked. It was also unmaintainable. When I asked the model to “make it DRY,” it invented a for_each over a map of objects so baroque that I spent longer reviewing the loop than I would have spent writing the eleven blocks by hand. That was the afternoon I started reaching for CDKTF instead — because the thing AI is genuinely good at is writing programs, and CDKTF lets your infrastructure be a program.

This post is about that workflow: using a model to scaffold CDK for Terraform constructs in TypeScript, running the synth-to-plan loop, and — most importantly — never letting the model anywhere near apply.

When code beats HCL

HCL is a fine configuration language and a frustrating programming language. The moment your infrastructure has real logic — conditional resources, computed naming, fan-out across environments, shared abstractions — you start fighting count, for_each, dynamic blocks, and the templatefile function. CDKTF lets you express all of that in TypeScript (or Python, Go, Java, C#) and generates the Terraform JSON for you.

Reach for CDKTF when:

  • You have strong abstractions to share — a “standard service” that bundles a load balancer, an ECS task, an autoscaling policy, and alarms.
  • You need real control flow — loops, conditionals, and computation that HCL makes painful.
  • Your team already lives in TypeScript and wants type checking and IDE autocomplete on infrastructure.

Stick with plain HCL when the config is genuinely static and flat. Don’t add a compiler to describe one S3 bucket.

This is also exactly where AI assistance pays off. A model is far better at generating idiomatic TypeScript classes than it is at generating clever HCL meta-arguments, because it has seen orders of magnitude more application code than infrastructure code.

Scaffolding the project

Start by letting the CLI lay down the skeleton — not the AI. The init command wires up the provider, the language, and the lockfile correctly:

# Install the toolchain
npm install --global cdktf-cli

# Scaffold a TypeScript project with the AWS provider
mkdir infra && cd infra
cdktf init --template=typescript --providers=aws@~>5.0 --local

That --local flag keeps state on your laptop for now; we will talk about state shortly. The generated main.ts is your entry point, and that is where I hand the wheel to a model.

Letting AI write the constructs

Here is the prompt pattern I use with Claude or Cursor: give it the construct boundary and the desired interface, not a blank page.

“Write a CDKTF TypeScript construct WebService that takes a name, container image, and desired count. It should create an aws_ecs_service, an aws_ecs_task_definition, and a aws_cloudwatch_metric_alarm on CPU. Use the AWS provider already imported. Do not call deploy or apply — only define resources.”

The output is real CDKTF, and it should look like this:

import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { EcsService } from "@cdktf/provider-aws/lib/ecs-service";
import { EcsTaskDefinition } from "@cdktf/provider-aws/lib/ecs-task-definition";
import { CloudwatchMetricAlarm } from "@cdktf/provider-aws/lib/cloudwatch-metric-alarm";

interface WebServiceProps {
  image: string;
  desiredCount: number;
  clusterArn: string;
}

class WebService extends Construct {
  public readonly service: EcsService;

  constructor(scope: Construct, id: string, props: WebServiceProps) {
    super(scope, id);

    const task = new EcsTaskDefinition(this, "task", {
      family: id,
      cpu: "256",
      memory: "512",
      networkMode: "awsvpc",
      requiresCompatibilities: ["FARGATE"],
      containerDefinitions: JSON.stringify([
        { name: id, image: props.image, essential: true },
      ]),
    });

    this.service = new EcsService(this, "service", {
      name: id,
      cluster: props.clusterArn,
      taskDefinition: task.arn,
      desiredCount: props.desiredCount,
      launchType: "FARGATE",
    });

    new CloudwatchMetricAlarm(this, "cpu-alarm", {
      alarmName: `${id}-cpu-high`,
      comparisonOperator: "GreaterThanThreshold",
      evaluationPeriods: 2,
      metricName: "CPUUtilization",
      namespace: "AWS/ECS",
      period: 60,
      statistic: "Average",
      threshold: 80,
    });
  }
}

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new AwsProvider(this, "aws", { region: "us-east-1" });

    new WebService(this, "checkout", {
      image: "ghcr.io/acme/checkout:latest",
      desiredCount: 3,
      clusterArn: "arn:aws:ecs:us-east-1:111122223333:cluster/prod",
    });
  }
}

const app = new App();
new MyStack(app, "prod-services");
app.synth();

Notice what the model gave you: a reusable class, a typed props interface, and a single call site. Spinning up a second service is one more new WebService(...). That is the leverage HCL never quite delivers.

Pro Tip: Always tell the model to import resources from the provider’s typed bindings (@cdktf/provider-aws/lib/...) rather than the generic TerraformResource escape hatch. Generated code that uses the escape hatch loses type safety and is far more likely to drift from the real schema.

The synth-to-plan workflow

This is the heart of why CDKTF is safe to use with AI. Your TypeScript never touches the cloud directly. It synthesizes down to ordinary Terraform JSON, and from there it is plain old Terraform that you can read, plan, and gate.

# 1. Compile TypeScript -> Terraform JSON. No cloud access happens here.
cdktf synth

# This writes cdktf.out/stacks/prod-services/cdktf.json
# It is just Terraform JSON config — open it and read it.

The synthesized output is human-reviewable:

{
  "resource": {
    "aws_ecs_service": {
      "checkout_service_…": {
        "name": "checkout",
        "desired_count": 3,
        "launch_type": "FARGATE"
      }
    }
  }
}

From there you have two equivalent paths to a plan. The CDKTF wrapper:

# diff is cdktf's word for `terraform plan`
cdktf diff prod-services

Or drop straight into native Terraform against the synthesized output:

cd cdktf.out/stacks/prod-services
terraform init
terraform plan -out=tf.plan

Either way you end up staring at a real Terraform plan — the resources to add, change, and destroy — before anything is applied. The model produced the code; the plan is the artifact a human signs off on. Feed that diff back into a review surface like Cursor, GitHub Copilot, or our own code review dashboard and ask whether the change matches intent. Treat the answer as advice, not approval.

Reviewing what the model wrote

A model is a fast junior engineer: quick, eager, occasionally confidently wrong. The plan output is your code review, and it catches the specific ways generated CDKTF goes sideways.

Things I look for on every AI-generated diff:

  • Hallucinated attributes. The model may set memoryReservation on a resource that wants memory, or invent a property that does not exist in the provider version you pinned. cdktf synth usually catches these as TypeScript errors — which is a feature, not a bug.
  • Destroy-and-recreate surprises. A changed family or name can force replacement of a stateful resource. The plan shows -/+; never skim past it.
  • Over-broad IAM. Generated policies love "Action": "*". Scope them down by hand.
  • Implicit deletions. If the model refactored a loop, resources can silently drop out of the config and the plan will queue them for destruction.

Pro Tip: Pin your provider version (aws@~>5.0) and commit cdktf.lock.hcl plus package-lock.json. AI tends to write against “latest,” and an unpinned provider means the JSON it synthesizes today may not match what runs in CI next week.

Never give the model the keys

Here is the line I will not cross, and you should not either: the AI writes code, and only code. It does not get cloud credentials, and it does not get state-write access.

Concretely:

  • No apply, ever, from the model. Generation and review run with no AWS credentials in the environment. cdktf synth needs zero cloud access — there is no reason for a key to be present when the model runs.
  • State lives behind a backend the model can’t reach. Move off --local to a remote backend (S3 + DynamoDB lock, or Terraform Cloud) and keep those credentials in CI/your shell, not in any agent context.
  • A human applies. cdktf deploy (which runs terraform apply) happens in a pipeline gated on an approved plan, or from a trusted operator’s terminal — after a person has read the diff.
# This is a HUMAN step, in CI or a trusted shell — never an agent action.
cdktf deploy prod-services   # prompts for approval, then terraform apply

The model’s job ends at the pull request. If your tooling ever makes it easy for an agent to go from “wrote some TypeScript” to “mutated production state” without a person reading the plan, you have built the wrong tooling. The same discipline applies whether you are scaffolding infra, drafting reusable prompts, or buying a prompt pack to standardize how your team asks for this kind of code.

Conclusion

CDKTF is the format that plays to an AI’s actual strengths: writing typed, abstracted, testable programs. The synth-to-plan workflow gives you a natural checkpoint — TypeScript compiles to Terraform JSON, JSON becomes a plan, and a human reads the plan before a single resource changes. Let the model be the fast junior engineer who scaffolds the construct. Keep the credentials, the state, and the apply firmly in human hands. Do that, and you get the speed of generated infrastructure without betting production on a model’s confidence.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.