Home
DevOps & Cloud Engineering / Lesson 21 — Pulumi & CDK — Code-First IaC

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:

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:

JavaScript
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:

Bash
pulumi preview      # like terraform plan
pulumi up           # like terraform apply
pulumi destroy

Power moves you couldn't do in Terraform:

JavaScript
// 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.

JavaScript
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:

Bash
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