Pulumi vs SST vs CDKTF: Infrastructure as Code in JavaScript (2026)
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
| Feature | Pulumi | SST | CDKTF |
|---|---|---|---|
| Language | TS, Python, Go, C# | TypeScript | TS, Python, Go, C# |
| Cloud support | Multi-cloud (100+) | AWS only | Multi-cloud (Terraform) |
| State management | Pulumi Cloud/self-hosted | AWS (CloudFormation) | Terraform state |
| Full-stack apps | Manual | ✅ (Next.js, Remix, Astro) | Manual |
| Live debugging | ❌ | ✅ (sst dev) | ❌ |
| Resource linking | Manual | ✅ (automatic) | Manual |
| Preview/diff | ✅ (pulumi preview) | ✅ (sst diff) | ✅ (cdktf diff) |
| Component model | ComponentResource | Built-in constructs | Constructs |
| Provider count | 100+ | AWS only | 3000+ (Terraform) |
| Secrets | ✅ (built-in encryption) | ✅ (via SSM) | ✅ (Terraform) |
| Policy-as-code | ✅ (CrossGuard) | ❌ | ✅ (Sentinel) |
| Import existing | ✅ | ❌ | ✅ |
| Pricing | Free tier + paid | Free (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.