CDK / Pulumi: IaC in General-Purpose Languages
What This Concept Is
Two families of IaC tools that use general-purpose programming languages (TypeScript, Python, Go, C#, Java) instead of a domain-specific language like HCL.
AWS CDK (Cloud Development Kit). You write resources as objects in TypeScript / Python / Java / C# / Go. The CDK synthesizes a CloudFormation template and deploys it via CloudFormation. State and drift are CloudFormation's, not a separate file.
Pulumi. Same idea, but broader: backs multiple clouds (AWS, Azure, GCP, Kubernetes) via provider plugins. State lives in a Pulumi-hosted or self-hosted backend, similar to Terraform's state. Supports the Terraform provider ecosystem through a bridge.
Both let you use loops, functions, classes, package managers, and unit testing frameworks. You get IDE autocomplete and refactoring tools for free.
Why It Matters Here
HCL is great at describing static end states. It struggles when the shape of the infrastructure depends on complex logic (per-region fanout, conditional subnets, feature flags driving resource creation). Teams hit this wall and reach for CDK or Pulumi -- or use HCL's for_each and dynamic blocks to the point that the config becomes harder to read than TypeScript would have been.
But using a general-purpose language also loses something: the clean separation between "desired state" and "code that computes desired state." A bug in a loop can explode into hundreds of resources. You are back in a world where subtle language features (variable capture, async, exceptions) affect infrastructure.
This is a genuine design tradeoff, not a clear win for either side.
Concrete Example: AWS CDK (TypeScript)
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as s3 from "aws-cdk-lib/aws-s3";
export class PlatformStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "Vpc", {
maxAzs: 3,
natGateways: 1,
});
const environments = ["dev", "staging", "prod"];
for (const env of environments) {
new s3.Bucket(this, `Artifacts-${env}`, {
bucketName: `acme-artifacts-${env}`,
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
});
}
}
}
CDK's Vpc construct creates ~20 CloudFormation resources behind the scenes. This is a "higher-level primitive" pattern that HCL modules also allow, but CDK constructs ship with AWS, curated.
Concrete Example: Pulumi (Python)
import pulumi
import pulumi_aws as aws
vpc = aws.ec2.Vpc(
"main",
cidr_block="10.0.0.0/16",
tags={"Name": "main"},
)
for i, az in enumerate(["us-east-1a", "us-east-1b", "us-east-1c"]):
aws.ec2.Subnet(
f"public-{az}",
vpc_id=vpc.id,
cidr_block=f"10.0.{i}.0/24",
availability_zone=az,
tags={"Name": f"public-{az}"},
)
pulumi.export("vpc_id", vpc.id)
Notice:
vpc.idis not a string -- it is apulumi.Outputthat resolves at apply time, similar to Terraform's "known after apply."- The for loop is plain Python. You get
enumerate, f-strings, and anything else the language offers. pulumi.export(...)is the equivalent ofoutput.
Where Each Shines (and Stumbles)
| Need | Terraform (HCL) | CDK | Pulumi |
|---|---|---|---|
| Tool ubiquity / hiring market | Largest | Medium (AWS-only) | Medium |
| Static, declarative stacks | Best | Good | Good |
| Complex logic / dynamic fanout | Awkward (for_each, dynamic) | Good | Best |
| Unit testing the infra code | Weak | Good (Jest, pytest) | Best |
| Cross-cloud portability | Yes (per-provider) | AWS only | Yes |
| Multi-language team | HCL only | TypeScript / Python / Java / C# / Go | TypeScript / Python / Go / .NET / Java / YAML |
| State model | Terraform state | CloudFormation | Pulumi state |
| Policy as code | Sentinel / OPA | CDK Aspects | Pulumi CrossGuard |
| Abstraction ceiling | HCL modules | CDK Constructs (L1/L2/L3) | Components |
Neither choice is wrong. Teams with strong TS/Python engineering culture often prefer CDK or Pulumi. Teams with mixed skill sets and strong operator culture often prefer Terraform's smaller surface.
Common Confusion / Misconception
"CDK is just a frontend for CloudFormation, so it has CloudFormation's limits." True. If CloudFormation does not support a feature or a provider, CDK does not either. Pulumi, by contrast, uses Terraform providers under the hood for many resources, so coverage is closer to Terraform's.
"Pulumi has no state." It has state, usually stored in Pulumi's managed service or an S3 backend, with locking semantics similar to Terraform.
"A general-purpose language is always more powerful." Power includes the power to write bad code. HCL's limited expressiveness is also a feature: fewer foot-guns. Some shops deliberately stay on HCL to keep infra simple.
"I can migrate between these tools easily." You cannot. Each tool owns its own state model. Migrations are project-scale efforts involving import-equivalent operations on both sides.
How To Use It
- Default to Terraform unless you have a specific reason to pick CDK or Pulumi. The reason should be concrete (cross-product test suite, heavy dynamic logic, existing TS/Python culture).
- If on CDK, invest in unit tests of your constructs. Language power + no tests = a bigger blast radius than HCL ever had.
- If on Pulumi, still review plans (
pulumi preview) the way you review Terraform plans. General-purpose language does not change the reading discipline. - Keep the IaC boundary tight regardless of tool. Do not let TypeScript infra code import from your app's TypeScript code; the coupling will bite you.
Check Yourself
- Name one category of infrastructure problem where Pulumi or CDK is clearly a better fit than HCL.
- Why does "unit test your CDK constructs" matter more than "unit test your HCL modules"?
- You have a team of Terraform-fluent engineers and no TypeScript experience. What is the cost of moving to CDK that blog posts typically understate?
Mini Drill or Application
Take one module you wrote in HCL (Concept 07). In 30 minutes, translate it -- not the whole thing, but the input/output contract -- into either CDK (TypeScript) or Pulumi (Python). Do not build infrastructure; just write the code.
Write one paragraph at the bottom comparing:
- which version is easier to read for someone new
- which version is easier to extend
- which version you would ship
You are developing taste, not certainty.
See also (external)
- AWS CDK v2: Developer Guide home -- constructs, the App/Stack model, synth, and deploy.
- Pulumi: Concepts -- projects, stacks, state, and the programming-model vs Terraform comparison.
Source Backbone
Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.
- Pro Git - versioned infrastructure changes, branching, review, and rollback habits.
- Git from the Bottom Up - mental model for stateful change history.
- The Linux Command Line - shell and automation grounding for infrastructure work.