Skip to main content

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.id is not a string -- it is a pulumi.Output that 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 of output.

Where Each Shines (and Stumbles)

NeedTerraform (HCL)CDKPulumi
Tool ubiquity / hiring marketLargestMedium (AWS-only)Medium
Static, declarative stacksBestGoodGood
Complex logic / dynamic fanoutAwkward (for_each, dynamic)GoodBest
Unit testing the infra codeWeakGood (Jest, pytest)Best
Cross-cloud portabilityYes (per-provider)AWS onlyYes
Multi-language teamHCL onlyTypeScript / Python / Java / C# / GoTypeScript / Python / Go / .NET / Java / YAML
State modelTerraform stateCloudFormationPulumi state
Policy as codeSentinel / OPACDK AspectsPulumi CrossGuard
Abstraction ceilingHCL modulesCDK 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

  1. 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).
  2. If on CDK, invest in unit tests of your constructs. Language power + no tests = a bigger blast radius than HCL ever had.
  3. If on Pulumi, still review plans (pulumi preview) the way you review Terraform plans. General-purpose language does not change the reading discipline.
  4. 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

  1. Name one category of infrastructure problem where Pulumi or CDK is clearly a better fit than HCL.
  2. Why does "unit test your CDK constructs" matter more than "unit test your HCL modules"?
  3. 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)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.