Pulumi & CDK — Code-First IaC
When real programming languages beat Terraform's HCL. Tradeoffs, when to use each.
Why Code-First IaC Exists
Terraform's HCL is purpose-built for infrastructure. It's declarative, predictable, and reasonably readable. But:
- It's not Turing-complete (intentionally — but limiting for complex logic)
- Loops and conditionals feel awkward (
count,for_each, ternary expressions) - You can't import normal libraries
- Testing infrastructure logic is awkward
- Onboarding requires learning a new DSL
Code-first IaC tools let you define infrastructure in real programming languages — TypeScript, Python, Go, C#, Java. The two main contenders:
Pulumi — multi-cloud, multi-language, independent vendor.
AWS CDK — AWS-specific (mostly), open source, AWS-maintained.
Both compile your code into deployable infrastructure. Both can manage the same things Terraform manages.
Should you switch from Terraform? Usually no — Terraform is the industry standard. But for specific cases (complex programmatic logic, heavy code reuse, teams that already think in code), code-first IaC is genuinely better.
Pulumi
Pulumi feels like writing a normal app, except the "side effect" is provisioning infrastructure.
A complete Pulumi program in TypeScript:
import * as aws from "@pulumi/aws";
// VPC
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
tags: { Name: "main" },
});
// Subnet for each AZ
const azs = ["us-east-1a", "us-east-1b", "us-east-1c"];
const subnets = azs.map((az, i) => new aws.ec2.Subnet(`subnet-${i}`, {
vpcId: vpc.id,
availabilityZone: az,
cidrBlock: `10.0.${i + 1}.0/24`,
}));
// Database in those subnets
const dbSubnetGroup = new aws.rds.SubnetGroup("db", {
subnetIds: subnets.map(s => s.id),
});
const db = new aws.rds.Instance("main", {
instanceClass: "db.t3.medium",
engine: "postgres",
engineVersion: "16",
allocatedStorage: 20,
dbSubnetGroupName: dbSubnetGroup.name,
skipFinalSnapshot: true,
});
// Export the database endpoint
export const dbEndpoint = db.endpoint;
Run:
pulumi preview # like terraform plan
pulumi up # like terraform apply
pulumi destroy
Power moves you couldn't do in Terraform:
// Real loops
const services = ["users", "orders", "billing"];
const clusters = services.map(name =>
new aws.ecs.Cluster(`${name}-cluster`, { name })
);
// Conditional logic
if (config.environment === "production") {
new aws.cloudwatch.MetricAlarm(...)
}
// Helper functions
function makeService(name: string, image: string) {
const role = new aws.iam.Role(`${name}-role`, {...});
const task = new aws.ecs.TaskDefinition(`${name}-task`, {...});
const service = new aws.ecs.Service(`${name}`, {...});
return service;
}
You import npm/pip packages, write tests, use your IDE's autocomplete fully. For complex programmatic infrastructure, this is a real win.
State and backends work like Terraform — Pulumi has its own SaaS, or you can store state in S3, GCS, etc.
AWS CDK
CDK (Cloud Development Kit) is AWS's code-first alternative. Compiles to CloudFormation under the hood.
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
class MyStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'MyVpc', {
maxAzs: 3,
natGateways: 1,
});
const db = new rds.DatabaseInstance(this, 'MyDb', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16,
}),
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
allocatedStorage: 20,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}
const app = new cdk.App();
new MyStack(app, 'MyStack');
Deploy:
cdk synth # compile to CloudFormation YAML
cdk diff # show what would change
cdk deploy
cdk destroy
CDK strengths over Terraform/Pulumi:
• Tightly integrated with AWS — new services often appear in CDK first
• Constructs library — high-level patterns (CDK provides a LoadBalancedFargateService that creates an ALB, target group, ECS service, task definition all in one line)
• Free (CloudFormation handles state — no external state file)
• Solid stack management (CloudFormation has good rollback semantics)
CDK weaknesses:
• AWS-only (mostly — there's CDK for Terraform "CDKTF" for multi-cloud)
• CloudFormation underneath has its own quirks (slow updates, occasional stuck stacks)
• Smaller community than Terraform
Picking Between Terraform, Pulumi, CDK
The 2026 decision tree:
Use Terraform if:
• Your team or industry already uses it (90%+ of cases)
• You want maximum portability across clouds
• You want the broadest community / module ecosystem
• You value simplicity over expressiveness
Use Pulumi if:
• You want to use real programming languages
• Your team thinks in TypeScript/Python more than YAML
• You have complex programmatic infrastructure logic
• You want multi-cloud with code-first ergonomics
Use AWS CDK if:
• You're 100% on AWS
• You want bleeding-edge AWS service support
• Your team likes constructs (high-level abstractions)
• You're okay with CloudFormation as the state layer
Most teams stick with Terraform. The "should we move to Pulumi/CDK?" debate is healthy but rarely a clear win — moving infrastructure off Terraform is a multi-month project that delivers maybe 20% productivity gain. Better to be excellent at Terraform than okay at Pulumi.
But for new greenfield infrastructure with a strongly-typed-language-loving team: Pulumi is genuinely lovely to work with. Try it on a small project and see.
The next lesson covers configuration management — Ansible and friends. Different problem from IaC: not "create this server" but "configure THIS server.
⁂ Back to all modules