Infrastructure as Code 2026: Pulumi vs Terraform CDK vs AWS CDK

The Infrastructure as Code landscape has evolved beyond HCL. Modern teams are choosing between Pulumi's true programming languages, Terraform's CDK for familiar syntax, and AWS CDK for cloud-native development. This comprehensive guide compares all three with real examples, migration strategies, and 2026 best practices.

The Evolution of Infrastructure as Code

Infrastructure as Code has come a long way since the days of manual server provisioning and configuration drift. The journey from shell scripts to declarative configuration languages represented a massive leap forward. But in 2026, we're seeing another paradigm shift: infrastructure defined using general-purpose programming languages.

The HCL Era

Terraform's HCL (HashiCorp Configuration Language) revolutionized infrastructure management with its declarative approach. It was simple, readable, and purpose-built for infrastructure. But as systems grew more complex, teams started hitting limitations:

The Programming Language Revolution

The new generation of IaC tools embraces general-purpose programming languages. This brings:

πŸ’‘ Key Insight

The shift to programming languages isn't about making infrastructure more complexβ€”it's about making it more maintainable. Teams can apply the same software engineering practices they use for application code to their infrastructure.

Tool Comparison

Let's compare the three leading modern IaC tools across key dimensions:

Feature Pulumi Terraform CDK AWS CDK
Primary Languages TypeScript, Python, Go, C#, Java TypeScript, Python, Java, C#, Go TypeScript, Python, Java, C#, Go
State Management Pulumi Cloud (default), self-hosted Terraform state (local, S3, etc.) CloudFormation (managed)
Cloud Providers 70+ providers All Terraform providers AWS (native), others via CFN
Execution Model Direct API calls Generates Terraform HCL Generates CloudFormation
Testing Unit, integration, policy-as-code Limited (via Terraform) Unit tests, integration tests
Pricing Free (self-hosted), $$$ (Pulumi Cloud) Free (open source) Free (AWS service)
Learning Curve Medium High (requires Terraform knowledge) Medium (AWS-focused)
Best For Multi-cloud, modern teams Existing Terraform users AWS-centric organizations

Deep Dive: Pulumi

Pulumi is the most mature of the "true" programming language IaC tools. It doesn't generate intermediate configurationβ€”it makes direct API calls to cloud providers using your code.

Architecture

# Pulumi Architecture
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Your Code (TS/Python/Go/C#/Java)     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚   Resources β”‚  β”‚   Logic     β”‚  β”‚   Tests     β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Pulumi CLI + Language Runtime              β”‚
β”‚         (Executes your program, builds resource graph)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Pulumi Cloud / Self-Hosted State           β”‚
β”‚              (Stores state, manages concurrency)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Cloud Provider APIs (AWS, Azure, GCP...)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Getting Started with Pulumi

# Install Pulumi
curl -fsSL https://get.pulumi.com | sh

# Create a new project
pulumi new aws-typescript

# Preview changes
pulumi preview

# Deploy
pulumi up

# Destroy
pulumi destroy

Pulumi Example: EKS Cluster

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";

// Configuration
const config = new pulumi.Config();
const minClusterSize = config.getNumber("minClusterSize") || 3;
const maxClusterSize = config.getNumber("maxClusterSize") || 6;
const desiredClusterSize = config.getNumber("desiredClusterSize") || 3;
const eksNodeInstanceType = config.get("eksNodeInstanceType") || "t3.medium";
const vpcNetworkCidr = config.get("vpcNetworkCidr") || "10.0.0.0/16";

// Create VPC
const vpc = new awsx.ec2.Vpc("eks-vpc", {
    enableDnsHostnames: true,
    cidrBlock: vpcNetworkCidr,
});

// Create EKS Cluster
const cluster = new eks.Cluster("eks-cluster", {
    vpcId: vpc.vpcId,
    subnetIds: vpc.publicSubnetIds,
    instanceType: eksNodeInstanceType,
    desiredCapacity: desiredClusterSize,
    minSize: minClusterSize,
    maxSize: maxClusterSize,
    nodeAmiId: "auto",
    version: "1.29",
    // Enable OIDC provider for IRSA
    createOidcProvider: true,
    // Cluster access
    providerCredentialOpts: {
        profileName: "default",
    },
});

// Export values
export const kubeconfig = cluster.kubeconfig;
export const vpcId = vpc.vpcId;
export const clusterName = cluster.eksCluster.name;
export const clusterEndpoint = cluster.eksCluster.endpoint;

// Create a Kubernetes provider
const k8sProvider = new k8s.Provider("k8s", {
    kubeconfig: cluster.kubeconfig,
});

// Deploy NGINX using the Kubernetes provider
const nginxDeployment = new k8s.apps.v1.Deployment("nginx", {
    metadata: { labels: { app: "nginx" } },
    spec: {
        replicas: 2,
        selector: { matchLabels: { app: "nginx" } },
        template: {
            metadata: { labels: { app: "nginx" } },
            spec: {
                containers: [{
                    name: "nginx",
                    image: "nginx:alpine",
                    ports: [{ containerPort: 80 }],
                }],
            },
        },
    },
}, { provider: k8sProvider });

// Create a load balancer service
const nginxService = new k8s.core.v1.Service("nginx", {
    metadata: { labels: { app: "nginx" } },
    spec: {
        type: "LoadBalancer",
        ports: [{ port: 80, targetPort: 80 }],
        selector: { app: "nginx" },
    },
}, { provider: k8sProvider });

export const nginxUrl = nginxService.status.loadBalancer.ingress[0].hostname;

Pulumi's Unique Features

βœ… Pulumi Strengths

Deep Dive: Terraform CDK

Terraform CDK (Cloud Development Kit) allows you to define infrastructure using familiar programming languages while still leveraging Terraform's robust provider ecosystem and state management.

How CDKTF Works

# CDKTF Workflow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Your Code (TS/Python/C#/Java/Go)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CDKTF CLI synthesizes code                 β”‚
β”‚              β†’ Generates Terraform JSON                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Terraform Core                             β”‚
β”‚              (Plan, Apply, State Management)            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Terraform Providers (AWS, Azure, GCP...)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Getting Started with CDKTF

# Install CDKTF
npm install -g cdktf-cli

# Create a new project
cdktf init --template="typescript" --providers="aws@~>5.0"

# Add providers
cdktf provider add "kubernetes@~>2.0"

# Synthesize (generate Terraform JSON)
cdktf synth

# Deploy (runs terraform apply)
cdktf deploy

# Destroy
cdktf destroy

CDKTF Example: VPC and EC2

import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Vpc } from "@cdktf/provider-aws/lib/vpc";
import { Subnet } from "@cdktf/provider-aws/lib/subnet";
import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway";
import { RouteTable } from "@cdktf/provider-aws/lib/route-table";
import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association";
import { Route } from "@cdktf/provider-aws/lib/route";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";
import { Instance } from "@cdktf/provider-aws/lib/instance";
import { DataAwsAmi } from "@cdktf/provider-aws/lib/data-aws-ami";

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

    // Configure AWS Provider
    new AwsProvider(this, "AWS", {
      region: "us-west-2",
    });

    // Create VPC
    const vpc = new Vpc(this, "vpc", {
      cidrBlock: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      tags: {
        Name: "cdktf-vpc",
      },
    });

    // Create Internet Gateway
    const igw = new InternetGateway(this, "igw", {
      vpcId: vpc.id,
      tags: {
        Name: "cdktf-igw",
      },
    });

    // Create Public Subnet
    const publicSubnet = new Subnet(this, "public-subnet", {
      vpcId: vpc.id,
      cidrBlock: "10.0.1.0/24",
      availabilityZone: "us-west-2a",
      mapPublicIpOnLaunch: true,
      tags: {
        Name: "cdktf-public-subnet",
      },
    });

    // Create Route Table
    const publicRouteTable = new RouteTable(this, "public-rt", {
      vpcId: vpc.id,
      tags: {
        Name: "cdktf-public-rt",
      },
    });

    // Create Route to Internet Gateway
    new Route(this, "internet-route", {
      routeTableId: publicRouteTable.id,
      destinationCidrBlock: "0.0.0.0/0",
      gatewayId: igw.id,
    });

    // Associate Route Table with Subnet
    new RouteTableAssociation(this, "public-rta", {
      subnetId: publicSubnet.id,
      routeTableId: publicRouteTable.id,
    });

    // Create Security Group
    const securityGroup = new SecurityGroup(this, "sg", {
      name: "cdktf-sg",
      vpcId: vpc.id,
      ingress: [
        {
          fromPort: 22,
          toPort: 22,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
        {
          fromPort: 80,
          toPort: 80,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
        {
          fromPort: 443,
          toPort: 443,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      egress: [
        {
          fromPort: 0,
          toPort: 0,
          protocol: "-1",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      tags: {
        Name: "cdktf-sg",
      },
    });

    // Get Latest Amazon Linux 2 AMI
    const ami = new DataAwsAmi(this, "ami", {
      mostRecent: true,
      owners: ["amazon"],
      filter: [
        {
          name: "name",
          values: ["amzn2-ami-hvm-*-x86_64-gp2"],
        },
      ],
    });

    // Create EC2 Instance
    const instance = new Instance(this, "web-server", {
      ami: ami.id,
      instanceType: "t3.micro",
      subnetId: publicSubnet.id,
      vpcSecurityGroupIds: [securityGroup.id],
      associatePublicIpAddress: true,
      userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "

Hello from CDKTF!

" > /var/www/html/index.html`, tags: { Name: "cdktf-web-server", }, }); // Outputs new TerraformOutput(this, "vpc_id", { value: vpc.id, }); new TerraformOutput(this, "public_ip", { value: instance.publicIp, }); new TerraformOutput(this, "instance_id", { value: instance.id, }); } } const app = new App(); new MyStack(app, "cdktf-aws"); app.synth();

CDKTF Constructs

CDKTF uses the Constructs library for abstraction:

import { Construct } from "constructs";
import { TerraformStack } from "cdktf";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";

interface WebServerSecurityGroupProps {
  readonly vpcId: string;
  readonly allowedCidrBlocks: string[];
  readonly enableHttps?: boolean;
}

export class WebServerSecurityGroup extends Construct {
  public readonly securityGroup: SecurityGroup;

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

    const ingressRules = [
      {
        fromPort: 22,
        toPort: 22,
        protocol: "tcp",
        cidrBlocks: props.allowedCidrBlocks,
        description: "SSH",
      },
      {
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        cidrBlocks: ["0.0.0.0/0"],
        description: "HTTP",
      },
    ];

    if (props.enableHttps) {
      ingressRules.push({
        fromPort: 443,
        toPort: 443,
        protocol: "tcp",
        cidrBlocks: ["0.0.0.0/0"],
        description: "HTTPS",
      });
    }

    this.securityGroup = new SecurityGroup(this, "sg", {
      name: `${id}-sg`,
      vpcId: props.vpcId,
      ingress: ingressRules,
      egress: [
        {
          fromPort: 0,
          toPort: 0,
          protocol: "-1",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      tags: {
        Name: `${id}-sg`,
      },
    });
  }
}

// Usage
const webSg = new WebServerSecurityGroup(this, "web", {
  vpcId: vpc.id,
  allowedCidrBlocks: ["10.0.0.0/8"],
  enableHttps: true,
});
⚠️ CDKTF Considerations

Deep Dive: AWS CDK

AWS CDK is Amazon's official infrastructure-as-code framework. While it can target other clouds via CloudFormation custom resources, it's primarily designed for AWS-first organizations.

AWS CDK Architecture

# AWS CDK Workflow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Your Code (TS/Python/Java/C#/Go)     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚   Constructsβ”‚  β”‚   Stacks    β”‚  β”‚   Apps      β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CDK CLI synthesizes code                   β”‚
β”‚              β†’ Generates CloudFormation Templates       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              AWS CloudFormation                         β”‚
β”‚              (Deployment, Drift Detection, Stack Sets)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              AWS Services                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Getting Started with AWS CDK

# Install CDK
npm install -g aws-cdk

# Create a new project
mkdir cdk-demo && cd cdk-demo
cdk init app --language=typescript

# Add constructs
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-eks

# Bootstrap (create S3 bucket for assets)
cdk bootstrap aws://ACCOUNT_ID/REGION

# Synthesize
cdk synth

# Deploy
cdk deploy

# Destroy
cdk destroy

AWS CDK Example: Serverless API

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class ServerlessApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // DynamoDB Table
    const table = new dynamodb.Table(this, 'ItemsTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      pointInTimeRecovery: true,
    });

    // Lambda Function
    const handler = new lambda.Function(this, 'ItemsHandler', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      environment: {
        TABLE_NAME: table.tableName,
      },
      logRetention: logs.RetentionDays.ONE_WEEK,
      tracing: lambda.Tracing.ACTIVE,
      memorySize: 256,
      timeout: cdk.Duration.seconds(10),
    });

    // Grant permissions
    table.grantReadWriteData(handler);

    // API Gateway
    const api = new apigw.RestApi(this, 'ItemsApi', {
      restApiName: 'Items Service',
      description: 'This service serves items.',
      deployOptions: {
        stageName: 'prod',
        tracingEnabled: true,
        loggingLevel: apigw.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: apigw.Cors.ALL_ORIGINS,
        allowMethods: apigw.Cors.ALL_METHODS,
      },
    });

    // API Endpoints
    const items = api.root.addResource('items');
    
    items.addMethod('GET', new apigw.LambdaIntegration(handler, {
      proxy: true,
    }), {
      methodResponses: [{
        statusCode: '200',
        responseModels: {
          'application/json': apigw.Model.EMPTY_MODEL,
        },
      }],
    });

    items.addMethod('POST', new apigw.LambdaIntegration(handler));

    const singleItem = items.addResource('{id}');
    singleItem.addMethod('GET', new apigw.LambdaIntegration(handler));
    singleItem.addMethod('PUT', new apigw.LambdaIntegration(handler));
    singleItem.addMethod('DELETE', new apigw.LambdaIntegration(handler));

    // Outputs
    new cdk.CfnOutput(this, 'APIEndpoint', {
      value: api.url,
      description: 'API Gateway endpoint URL',
    });

    new cdk.CfnOutput(this, 'TableName', {
      value: table.tableName,
      description: 'DynamoDB Table Name',
    });
  }
}

// Lambda code (lambda/index.js)
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME;

exports.handler = async (event) => {
  const method = event.httpMethod;
  const path = event.path;
  
  try {
    switch (method) {
      case 'GET':
        if (event.pathParameters?.id) {
          // Get single item
          const result = await dynamodb.get({
            TableName: TABLE_NAME,
            Key: { id: event.pathParameters.id },
          }).promise();
          return {
            statusCode: 200,
            body: JSON.stringify(result.Item),
          };
        } else {
          // List items
          const result = await dynamodb.scan({
            TableName: TABLE_NAME,
          }).promise();
          return {
            statusCode: 200,
            body: JSON.stringify(result.Items),
          };
        }
        
      case 'POST':
        const item = JSON.parse(event.body);
        item.id = Date.now().toString();
        await dynamodb.put({
          TableName: TABLE_NAME,
          Item: item,
        }).promise();
        return {
          statusCode: 201,
          body: JSON.stringify(item),
        };
        
      default:
        return {
          statusCode: 405,
          body: JSON.stringify({ error: 'Method not allowed' }),
        };
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message }),
    };
  }
};

AWS CDK Constructs Library

AWS CDK's biggest strength is its extensive library of high-level constructs:

// L3 Construct Example: ECS Fargate Service with ALB
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsp from 'aws-cdk-lib/aws-ecs-patterns';

new ecsp.ApplicationLoadBalancedFargateService(this, 'FargateService', {
  taskImageOptions: {
    image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
    containerPort: 80,
  },
  desiredCount: 3,
  cpu: 512,
  memoryLimitMiB: 2048,
  publicLoadBalancer: true,
  enableExecuteCommand: true,
});
// This single construct creates:
// - ECS Cluster
// - Fargate Task Definition
// - Application Load Balancer
// - Security Groups
// - CloudWatch Log Group
// - Auto-scaling policies
βœ… AWS CDK Strengths

Real-World Examples

Let's compare how each tool handles a common scenario: deploying a containerized application with a database.

Scenario: Containerized Web App with PostgreSQL

Pulumi Implementation

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

// VPC
const vpc = new awsx.ec2.Vpc("app-vpc", {
  cidrBlock: "10.0.0.0/16",
  numberOfAvailabilityZones: 2,
});

// Security Group for RDS
const dbSecurityGroup = new aws.ec2.SecurityGroup("db-sg", {
  vpcId: vpc.vpcId,
  ingress: [{
    protocol: "tcp",
    fromPort: 5432,
    toPort: 5432,
    securityGroups: [vpc.securityGroupIds[0]],
  }],
});

// RDS Subnet Group
const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnet-group", {
  subnetIds: vpc.privateSubnetIds,
});

// RDS Instance
const db = new aws.rds.Instance("postgres", {
  engine: "postgres",
  engineVersion: "15",
  instanceClass: "db.t3.micro",
  allocatedStorage: 20,
  dbName: "appdb",
  username: "admin",
  password: new pulumi.RandomPassword("db-password", {
    length: 16,
    special: true,
  }).result,
  vpcSecurityGroupIds: [dbSecurityGroup.id],
  dbSubnetGroupName: dbSubnetGroup.name,
  skipFinalSnapshot: true,
});

// ECS Cluster
const cluster = new aws.ecs.Cluster("app-cluster");

// ECR Repository
const repo = new aws.ecr.Repository("app-repo");

// ECS Task Definition
const taskDef = new aws.ecs.TaskDefinition("app-task", {
  family: "app-task",
  cpu: "256",
  memory: "512",
  networkMode: "awsvpc",
  requiresCompatibilities: ["FARGATE"],
  containerDefinitions: pulumi.all([db.endpoint, db.username, db.password]).apply(
    ([endpoint, username, password]) => JSON.stringify([{
      name: "app",
      image: repo.repositoryUrl,
      portMappings: [{ containerPort: 3000 }],
      environment: [
        { name: "DATABASE_URL", value: `postgres://${username}:${password}@${endpoint}/appdb` },
      ],
    }])
  ),
});

// ECS Service with ALB
const service = new awsx.ecs.FargateService("app-service", {
  cluster: cluster.arn,
  taskDefinition: taskDef.arn,
  desiredCount: 2,
  networkConfiguration: {
    subnets: vpc.publicSubnetIds,
    securityGroups: [vpc.securityGroupIds[0]],
    assignPublicIp: true,
  },
  loadBalancers: [{
    targetGroupArn: alb.defaultTargetGroup.arn,
    containerName: "app",
    containerPort: 3000,
  }],
});

export const appUrl = alb.loadBalancer.dnsName;
export const dbEndpoint = db.endpoint;

CDKTF Implementation

// Similar structure but generates Terraform JSON
// Uses Terraform AWS provider resources directly
// More verbose due to Terraform provider syntax

AWS CDK Implementation

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';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsp from 'aws-cdk-lib/aws-ecs-patterns';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';

export class ContainerAppStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, 'AppVpc', {
      maxAzs: 2,
    });

    // Database credentials
    const dbCredentials = new secretsmanager.Secret(this, 'DbCredentials', {
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: 'admin' }),
        generateStringKey: 'password',
        excludeCharacters: '"@/\\',
      },
    });

    // RDS Instance
    const db = new rds.DatabaseInstance(this, 'Postgres', {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_15,
      }),
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      credentials: rds.Credentials.fromSecret(dbCredentials),
      databaseName: 'appdb',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // ECS Cluster
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

    // Fargate Service with ALB
    const fargateService = new ecsp.ApplicationLoadBalancedFargateService(
      this,
      'AppService',
      {
        cluster,
        taskImageOptions: {
          image: ecs.ContainerImage.fromAsset('./app'),
          containerPort: 3000,
          environment: {
            DATABASE_URL: `postgres://${dbCredentials.secretValueFromJson('username').unsafeUnwrap()}:${dbCredentials.secretValueFromJson('password').unsafeUnwrap()}@${db.dbInstanceEndpointAddress}/appdb`,
          },
        },
        desiredCount: 2,
        cpu: 256,
        memoryLimitMiB: 512,
        publicLoadBalancer: true,
      }
    );

    // Allow service to connect to database
    db.connections.allowFrom(
      fargateService.service,
      ec2.Port.tcp(5432)
    );

    // Outputs
    new cdk.CfnOutput(this, 'AppUrl', {
      value: fargateService.loadBalancer.loadBalancerDnsName,
    });

    new cdk.CfnOutput(this, 'DbEndpoint', {
      value: db.dbInstanceEndpointAddress,
    });
  }
}

Migration Strategies

Migrating from existing infrastructure to a new IaC tool requires careful planning. Here are proven strategies:

Strategy 1: Import Existing Resources

# Pulumi: Import existing resources
pulumi import aws:ec2/instance:Instance myInstance i-1234567890abcdef0

# CDKTF: Use terraform import
cdktf import

# AWS CDK: Use CfnInclude for existing CloudFormation
cdk import

Strategy 2: Strangler Fig Pattern

  1. Identify Boundaries: Find natural service boundaries in your existing infrastructure.
  2. Create New Stack: Build the new service using your chosen IaC tool.
  3. Migrate Traffic: Gradually shift traffic to the new infrastructure.
  4. Decommission Old: Remove the old infrastructure once migration is complete.

Strategy 3: Side-by-Side Deployment

Run new and old infrastructure in parallel:

# Example: Blue/Green with Pulumi
const config = new pulumi.Config();
const deploymentMode = config.get("mode") || "blue";

const blueStack = deploymentMode === "blue" ? createBlueStack() : undefined;
const greenStack = deploymentMode === "green" ? createGreenStack() : undefined;

// Use stack references to coordinate
export const activeEndpoint = deploymentMode === "blue" 
  ? blueStack?.endpoint 
  : greenStack?.endpoint;

2026 Best Practices

General IaC Best Practices

Testing Strategies

// Pulumi: Unit testing with Jest
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect } from "@jest/globals";

pulumi.runtime.setMocks({
  newResource: (args: pulumi.runtime.MockResourceArgs): {id: string, state: any} => {
    return { id: args.inputs.name ? `${args.name}_id` : args.name + "_id", state: { ...args.inputs } };
  },
  call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
});

describe("Infrastructure Tests", () => {
  it("should create a VPC with correct CIDR", async () => {
    const infra = await import("../index");
    const vpcCidr = await pulumi.runtime.promiseResult(infra.vpc.cidrBlock);
    expect(vpcCidr).toBe("10.0.0.0/16");
  });
});

// AWS CDK: Testing with assertions
import { Template } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { MyStack } from '../lib/my-stack';

test('VPC Created with 2 AZs', () => {
  const app = new cdk.App();
  const stack = new MyStack(app, 'TestStack');
  const template = Template.fromStack(stack);
  
  template.hasResourceProperties('AWS::EC2::VPC', {
    CidrBlock: '10.0.0.0/16',
  });
  
  template.resourceCountIs('AWS::EC2::Subnet', 4); // 2 public + 2 private
});

CI/CD Integration

# .github/workflows/iac.yml
name: Infrastructure as Code

on:
  push:
    paths:
      - 'infrastructure/**'
  pull_request:
    paths:
      - 'infrastructure/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: cd infrastructure && npm ci
      
      - name: Run tests
        run: cd infrastructure && npm test
      
      - name: Lint
        run: cd infrastructure && npm run lint
      
      - name: Security scan
        run: cd infrastructure && npm run security:check

  preview:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.IAC_ROLE_ARN }}
          aws-region: us-west-2
      
      - name: Pulumi Preview
        uses: pulumi/actions@v5
        with:
          command: preview
          stack-name: staging
          work-dir: ./infrastructure
          comment-on-pr: true
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.IAC_ROLE_ARN }}
          aws-region: us-west-2
      
      - name: Pulumi Deploy
        uses: pulumi/actions@v5
        with:
          command: up
          stack-name: production
          work-dir: ./infrastructure
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

Decision Framework

Use this framework to choose the right tool for your situation:

If You... Choose Why
Are multi-cloud or want flexibility Pulumi Best multi-cloud support, true programming model
Have existing Terraform investment Terraform CDK Leverage existing modules and state
Are AWS-first and want rich constructs AWS CDK Best AWS integration, extensive L3 constructs
Need strong testing capabilities Pulumi or AWS CDK Better testing frameworks and IDE support
Want lowest learning curve AWS CDK (if AWS) / Pulumi Better documentation and examples
Need enterprise support Pulumi or Terraform CDK Enterprise support options available

Conclusion

The Infrastructure as Code landscape in 2026 offers more power and flexibility than ever before. Pulumi, Terraform CDK, and AWS CDK each bring unique strengths to the table.

Pulumi leads in multi-cloud flexibility and true programming language support. It's the best choice for teams that want to apply software engineering practices to infrastructure and need to work across multiple cloud providers.

Terraform CDK is the right choice for organizations with significant Terraform investments. It provides a migration path to programming languages while preserving existing modules and workflows.

AWS CDK is unbeatable for AWS-centric organizations. Its rich construct library and native integration make it the most productive choice for teams building on AWS.

The best tool is the one that fits your team's skills, your infrastructure requirements, and your operational constraints. All three are production-ready in 2026, and the gap between them continues to narrow. The important thing is to choose one, commit to it, and build expertise.

πŸš€ Next Steps

1. Audit your current infrastructure and pain points
2. Build a proof-of-concept with your top choice
3. Establish testing and CI/CD patterns
4. Migrate incrementally using the strangler fig pattern
5. Document patterns and build reusable components