Skip to main content

Pulumi vs SST vs CDKTF: Infrastructure as Code in JavaScript (2026)

·PkgPulse Team

TL;DR

Pulumi is the general-purpose IaC platform — define cloud infrastructure in TypeScript, Python, Go, supports all major clouds, state management, policy-as-code, the multi-cloud IaC leader. SST is the serverless framework for AWS — deploy Next.js, APIs, and databases to AWS with minimal config, live Lambda debugging, built-in constructs, purpose-built for full-stack apps. CDKTF (CDK for Terraform) brings AWS CDK's construct model to Terraform — write TypeScript constructs that generate Terraform HCL, leverage the massive Terraform provider ecosystem. In 2026: Pulumi for multi-cloud TypeScript IaC, SST for deploying full-stack apps to AWS, CDKTF for TypeScript + Terraform ecosystem.

Key Takeaways

  • Pulumi: @pulumi/pulumi ~200K weekly downloads — multi-cloud, TypeScript/Python/Go, state management
  • SST: sst ~50K weekly downloads — AWS, full-stack, Next.js/Remix, live Lambda debugging
  • CDKTF: cdktf ~50K weekly downloads — Terraform providers, TypeScript constructs, HCL output
  • Pulumi supports AWS, Azure, GCP, Kubernetes, and 100+ providers
  • SST has the best experience for deploying full-stack JavaScript apps to AWS
  • CDKTF leverages all existing Terraform providers and modules

Pulumi

Pulumi — multi-cloud IaC:

Basic AWS infrastructure

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

// S3 bucket:
const bucket = new aws.s3.Bucket("pkgpulse-assets", {
  website: {
    indexDocument: "index.html",
    errorDocument: "404.html",
  },
})

// Lambda function:
const fn = new aws.lambda.Function("api-handler", {
  runtime: "nodejs20.x",
  handler: "index.handler",
  code: new pulumi.asset.AssetArchive({
    ".": new pulumi.asset.FileArchive("./dist"),
  }),
  environment: {
    variables: {
      BUCKET_NAME: bucket.id,
    },
  },
})

// API Gateway:
const api = new aws.apigatewayv2.Api("http-api", {
  protocolType: "HTTP",
})

const integration = new aws.apigatewayv2.Integration("lambda-integration", {
  apiId: api.id,
  integrationType: "AWS_PROXY",
  integrationUri: fn.arn,
})

// Outputs:
export const bucketUrl = bucket.websiteEndpoint
export const apiUrl = api.apiEndpoint

Multi-cloud

import * as pulumi from "@pulumi/pulumi"
import * as aws from "@pulumi/aws"
import * as gcp from "@pulumi/gcp"
import * as cloudflare from "@pulumi/cloudflare"

// AWS — database:
const db = new aws.rds.Instance("pkgpulse-db", {
  engine: "postgres",
  instanceClass: "db.t4g.micro",
  allocatedStorage: 20,
  dbName: "pkgpulse",
})

// GCP — storage:
const bucket = new gcp.storage.Bucket("pkgpulse-backups", {
  location: "US",
  uniformBucketLevelAccess: true,
})

// Cloudflare — DNS:
const record = new cloudflare.Record("api-record", {
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
  name: "api",
  type: "CNAME",
  value: "api.pkgpulse.com",
  proxied: true,
})

Component resources

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

// Reusable component:
class StaticSite extends pulumi.ComponentResource {
  public readonly url: pulumi.Output<string>

  constructor(name: string, args: { domain: string }, opts?: pulumi.ComponentResourceOptions) {
    super("pkgpulse:StaticSite", name, {}, opts)

    const bucket = new aws.s3.Bucket(`${name}-bucket`, {
      website: { indexDocument: "index.html" },
    }, { parent: this })

    const cdn = new aws.cloudfront.Distribution(`${name}-cdn`, {
      origins: [{
        domainName: bucket.bucketRegionalDomainName,
        originId: "s3",
      }],
      enabled: true,
      defaultRootObject: "index.html",
      defaultCacheBehavior: {
        viewerProtocolPolicy: "redirect-to-https",
        allowedMethods: ["GET", "HEAD"],
        cachedMethods: ["GET", "HEAD"],
        targetOriginId: "s3",
        forwardedValues: { queryString: false, cookies: { forward: "none" } },
      },
      restrictions: { geoRestriction: { restrictionType: "none" } },
      viewerCertificate: { cloudfrontDefaultCertificate: true },
    }, { parent: this })

    this.url = cdn.domainName
    this.registerOutputs({ url: this.url })
  }
}

// Usage:
const site = new StaticSite("pkgpulse", { domain: "pkgpulse.com" })
export const siteUrl = site.url

SST

SST — full-stack AWS framework:

Next.js deployment

// sst.config.ts
export default $config({
  app(input) {
    return {
      name: "pkgpulse",
      removal: input.stage === "production" ? "retain" : "remove",
      home: "aws",
    }
  },
  async run() {
    // Deploy Next.js to AWS:
    const site = new sst.aws.Nextjs("PkgPulse", {
      domain: "pkgpulse.com",
      environment: {
        DATABASE_URL: db.properties.connectionString,
      },
    })

    return { url: site.url }
  },
})

Full-stack app

// sst.config.ts
export default $config({
  app(input) {
    return { name: "pkgpulse", home: "aws" }
  },
  async run() {
    // Database:
    const db = new sst.aws.Postgres("Database", {
      scaling: { min: "0.5 ACU", max: "4 ACU" },
    })

    // Storage:
    const bucket = new sst.aws.Bucket("Assets")

    // API:
    const api = new sst.aws.Function("Api", {
      handler: "packages/api/src/index.handler",
      url: true,
      link: [db, bucket],
      environment: {
        STAGE: $app.stage,
      },
    })

    // Cron job:
    new sst.aws.Cron("DailySync", {
      schedule: "rate(1 day)",
      job: {
        handler: "packages/jobs/src/sync.handler",
        link: [db],
      },
    })

    // Frontend:
    const site = new sst.aws.Nextjs("Web", {
      path: "packages/web",
      link: [api],
      domain: "pkgpulse.com",
    })

    return {
      api: api.url,
      site: site.url,
    }
  },
})

Linked resources

// In your Lambda function code:
import { Resource } from "sst"

// Type-safe resource access:
const dbUrl = Resource.Database.connectionString
const bucketName = Resource.Assets.name

// No manual env var wiring needed — SST links resources automatically

Live Lambda debugging

# Start live development:
npx sst dev

# → Deploys to AWS
# → Proxies Lambda invocations to your local machine
# → Set breakpoints in VS Code
# → See logs in real-time
# → Hot reload on file changes

CDKTF

CDKTF — CDK for Terraform:

Basic infrastructure

import { Construct } from "constructs"
import { App, TerraformStack, TerraformOutput } from "cdktf"
import { AwsProvider } from "@cdktf/provider-aws/lib/provider"
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket"
import { LambdaFunction } from "@cdktf/provider-aws/lib/lambda-function"
import { IamRole } from "@cdktf/provider-aws/lib/iam-role"

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

    new AwsProvider(this, "aws", { region: "us-east-1" })

    // S3 bucket:
    const bucket = new S3Bucket(this, "assets", {
      bucket: "pkgpulse-assets",
    })

    // IAM role:
    const role = new IamRole(this, "lambda-role", {
      name: "pkgpulse-lambda-role",
      assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
          Action: "sts:AssumeRole",
          Effect: "Allow",
          Principal: { Service: "lambda.amazonaws.com" },
        }],
      }),
    })

    // Lambda:
    const fn = new LambdaFunction(this, "api", {
      functionName: "pkgpulse-api",
      runtime: "nodejs20.x",
      handler: "index.handler",
      role: role.arn,
      filename: "dist/lambda.zip",
      environment: {
        variables: { BUCKET_NAME: bucket.id },
      },
    })

    new TerraformOutput(this, "bucket-name", { value: bucket.id })
    new TerraformOutput(this, "function-arn", { value: fn.arn })
  }
}

const app = new App()
new PkgPulseStack(app, "pkgpulse")
app.synth()

Terraform providers

import { CloudflareProvider } from "@cdktf/provider-cloudflare/lib/provider"
import { Record } from "@cdktf/provider-cloudflare/lib/record"
import { DataCloudflareZone } from "@cdktf/provider-cloudflare/lib/data-cloudflare-zone"

// Use any Terraform provider:
new CloudflareProvider(this, "cloudflare", {
  apiToken: process.env.CLOUDFLARE_API_TOKEN,
})

const zone = new DataCloudflareZone(this, "zone", {
  name: "pkgpulse.com",
})

new Record(this, "api-record", {
  zoneId: zone.id,
  name: "api",
  type: "CNAME",
  value: "api.pkgpulse.com",
  proxied: true,
})

Reusable constructs

import { Construct } from "constructs"
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket"
import { CloudfrontDistribution } from "@cdktf/provider-aws/lib/cloudfront-distribution"

class StaticSite extends Construct {
  public readonly url: string

  constructor(scope: Construct, id: string, props: { domain: string }) {
    super(scope, id)

    const bucket = new S3Bucket(this, "bucket", {
      bucket: `${props.domain}-static`,
    })

    const cdn = new CloudfrontDistribution(this, "cdn", {
      origin: [{
        domainName: bucket.bucketRegionalDomainName,
        originId: "s3",
      }],
      enabled: true,
      defaultRootObject: "index.html",
      defaultCacheBehavior: {
        allowedMethods: ["GET", "HEAD"],
        cachedMethods: ["GET", "HEAD"],
        targetOriginId: "s3",
        viewerProtocolPolicy: "redirect-to-https",
        forwardedValues: { queryString: false, cookies: { forward: "none" } },
      },
      restrictions: { geoRestriction: { restrictionType: "none" } },
      viewerCertificate: { cloudfrontDefaultCertificate: true },
    })

    this.url = cdn.domainName
  }
}

// Usage:
const site = new StaticSite(this, "site", { domain: "pkgpulse.com" })

CLI workflow

# Initialize:
cdktf init --template=typescript

# Synthesize (TypeScript → Terraform JSON):
cdktf synth

# Plan:
cdktf diff

# Deploy:
cdktf deploy

# Destroy:
cdktf destroy

Feature Comparison

FeaturePulumiSSTCDKTF
LanguageTS, Python, Go, C#TypeScriptTS, Python, Go, C#
Cloud supportMulti-cloud (100+)AWS onlyMulti-cloud (Terraform)
State managementPulumi Cloud/self-hostedAWS (CloudFormation)Terraform state
Full-stack appsManual✅ (Next.js, Remix, Astro)Manual
Live debugging✅ (sst dev)
Resource linkingManual✅ (automatic)Manual
Preview/diff✅ (pulumi preview)✅ (sst diff)✅ (cdktf diff)
Component modelComponentResourceBuilt-in constructsConstructs
Provider count100+AWS only3000+ (Terraform)
Secrets✅ (built-in encryption)✅ (via SSM)✅ (Terraform)
Policy-as-code✅ (CrossGuard)✅ (Sentinel)
Import existing
PricingFree tier + paidFree (open-source)Free (open-source)

When to Use Each

Use Pulumi if:

  • Need multi-cloud infrastructure (AWS + GCP + Azure)
  • Want a mature IaC platform with policy enforcement
  • Need to manage complex infrastructure at scale
  • Want full programming language features in your IaC

Use SST if:

  • Deploying full-stack JavaScript apps to AWS
  • Want live Lambda debugging during development
  • Building serverless apps with Next.js, Remix, or Astro
  • Want automatic resource linking (type-safe)

Use CDKTF if:

  • Already using Terraform or want access to Terraform providers
  • Need the massive Terraform module/provider ecosystem
  • Want TypeScript constructs that output Terraform HCL
  • Migrating from HCL Terraform to TypeScript

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on Pulumi v3.x, SST v3.x (Ion), and CDKTF v0.20.x.

Compare DevOps tooling and cloud libraries on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.