AWS CDK was a genuine step forward. Writing infrastructure in TypeScript, Python, or Go beat writing CloudFormation YAML by a wide margin, and the L2/L3 construct library made common patterns ergonomic. For AWS-only teams, CDK is still one of the better ways to manage infrastructure.
The friction shows up at the seams. CDK compiles to CloudFormation, which means you inherit CloudFormation's rollback behavior, its drift detection quirks, and its 500-resource-per-stack limit. Deploys are slow because CloudFormation is slow. The L2 constructs sometimes abstract the wrong things, leaving you reaching through escape hatches (node.defaultChild, addPropertyOverride) when the opinions don't fit. And because CDK is AWS-only, a multi-cloud strategy means running CDK plus something else.
This guide covers the realistic alternatives, from cross-cloud IaC tools to approaches that eliminate the separate-infrastructure-program model entirely.
| Feature | Encore | Pulumi | Terraform / OpenTofu | SST | Serverless Framework | AWS SAM |
|---|---|---|---|---|---|---|
| Approach | Infrastructure from code | IaC (imperative) | IaC (HCL) | IaC (CDK-based, serverless-first) | IaC (config-driven) | IaC (CloudFormation macro) |
| Language | TypeScript / Go | TypeScript, Python, Go, C#, Java | HCL | TypeScript | YAML + plugins | YAML |
| Cloud support | AWS, GCP (provisions in your account) | AWS, GCP, Azure, 150+ providers | AWS, GCP, Azure, 3,900+ providers | AWS only | AWS primary, multi-cloud plugins | AWS only |
| Backing engine | Direct API + Terraform provider | Pulumi engine (no CloudFormation) | Terraform state | CDK → CloudFormation | CloudFormation | CloudFormation |
| Infrastructure config | Zero (derived from app code) | Separate program | Separate HCL files | Separate config | Separate YAML | Separate YAML |
| State management | None | Managed (Pulumi Cloud) or self-managed | Remote state file | CloudFormation stacks | CloudFormation stacks | CloudFormation stacks |
| Deploy speed (simple stack) | Fast | Fast | Fast | Slow (CFN) | Slow (CFN) | Slow (CFN) |
| Multi-cloud | AWS + GCP native | Yes | Yes | No | Limited | No |
| License | Open Source (MPL-2.0) | Open Source (Apache 2.0) | MPL-2.0 (OpenTofu) / BSL (Terraform) | Open Source (MIT) | Open Source (MIT) | Open Source (Apache 2.0) |
| Best for | Teams who want zero infra management | Multi-cloud, CDK-like DX | Teams with existing Terraform estate | Serverless on AWS | Lambda-centric apps | AWS-native serverless |
Encore takes a fundamentally different approach from every other tool on this list. Instead of writing a separate infrastructure program that provisions resources and then a second application that uses them, you declare what your application needs directly in your TypeScript or Go code. Databases, Pub/Sub topics, cron jobs, object storage, and caching are declared as objects in the same files where you write your business logic. Encore's open-source tooling parses the code to understand the infrastructure requirements, and Encore Cloud provisions the corresponding AWS (or GCP) resources in your own account.
There's no CloudFormation stack, no construct tree, no separate program to build. The application code is the source of truth. Adding a database to a service is one line of code in the same file as the queries that use it.
CDK lets you write infrastructure in TypeScript, but the TypeScript is a CloudFormation generator, it describes resources in a parallel program. Encore's infrastructure declarations sit next to the code that uses them.
Encore provisions real AWS resources in your account (RDS, SQS, SNS, S3, EventBridge, CloudWatch). Production defaults are applied automatically (private VPC placement, encryption at rest, least-privilege IAM, CloudWatch logging), so you only configure what you want to override.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Topic } from "encore.dev/pubsub";
import { CronJob } from "encore.dev/cron";
// Provisions managed Postgres (RDS in AWS production; Docker locally).
const db = new SQLDatabase("orders", { migrations: "./migrations" });
// Provisions SNS topic + SQS subscriptions automatically.
interface OrderPlaced { orderId: string; userId: string; }
export const orderPlaced = new Topic<OrderPlaced>("order-placed", {
deliveryGuarantee: "at-least-once",
});
// Provisions EventBridge scheduled rule.
new CronJob("daily-reports", {
title: "Send daily reports",
every: "24h",
endpoint: generateReports,
});
export const placeOrder = api(
{ method: "POST", path: "/orders", expose: true },
async (req: PlaceOrder): Promise<Order> => {
const order = await db.queryRow`INSERT INTO orders ...`;
await orderPlaced.publish({ orderId: order.id, userId: req.userId });
return order;
},
);
Deploys are faster than CDK because Encore isn't routing through CloudFormation. Changes go directly to the AWS APIs (with a Terraform provider as an escape hatch for non-Encore resources).
Good fit for: teams on AWS or GCP who want to skip the construct tree, stack management, and CloudFormation slowness entirely.
Less good fit for: teams committed to Azure (works via Terraform provider), or teams with heavy existing CDK investment they don't want to migrate away from.
Groupon and other production teams use Encore to manage their AWS backends. It's open source (11k+ GitHub stars).
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
Pulumi is the closest direct competitor to CDK. You write infrastructure in TypeScript, Python, Go, C#, or Java, and Pulumi provisions resources directly through cloud SDKs, no CloudFormation in the middle.
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("my-bucket", {
versioning: { enabled: true },
});
export const bucketName = bucket.id;
Advantages over CDK:
Tradeoffs:
Good fit for: teams who like CDK's programming-language approach but need multi-cloud or want to shed CloudFormation.
Terraform (HashiCorp, BSL-licensed since 2023) and OpenTofu (MPL-2.0 fork) remain the most widely-used IaC tools. HCL is its own thing, state files are a known quantity, and the ecosystem of providers is enormous.
resource "aws_s3_bucket" "main" {
bucket = "my-bucket"
}
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
Advantages over CDK:
Tradeoffs:
Good fit for: teams with existing Terraform estates or who prefer declarative configuration. See our Terraform Alternatives page for more.
SST is built on top of CDK with a serverless-first opinion. It wraps CDK constructs into higher-level abstractions ("Api", "Bucket", "Table") aimed at Lambda + API Gateway + DynamoDB shaped applications.
import { StackContext, Api } from "sst/constructs";
export function API({ stack }: StackContext) {
const api = new Api(stack, "api", {
routes: { "GET /": "packages/functions/src/lambda.handler" },
});
stack.addOutputs({ ApiEndpoint: api.url });
}
Advantages over raw CDK:
sst dev runs your functions locally while using real AWS resources).Tradeoffs:
Good fit for: teams building Lambda-heavy AWS apps who want more ergonomic abstractions than raw CDK. See our SST Alternatives for a broader take.
Serverless Framework predates both CDK and SST. It's YAML-driven with a plugin ecosystem, focused on Lambda deployments, and works with multiple clouds via plugins.
service: my-service
provider:
name: aws
runtime: nodejs20.x
functions:
hello:
handler: handler.hello
events:
- httpApi: "GET /"
Advantages over CDK:
Tradeoffs:
Good fit for: small Lambda deployments where YAML config is enough.
AWS SAM (Serverless Application Model) is AWS's official serverless IaC tool. It's a CloudFormation macro that expands shorthand SAM resources into full CloudFormation.
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.handler
Runtime: nodejs20.x
Events:
Api:
Type: Api
Properties:
Path: /
Method: get
Advantages over CDK:
Tradeoffs:
Good fit for: teams deep in AWS who want the most official, AWS-supported path.
Stay on CDK if:
Move to Encore if:
Move to Pulumi if:
Move to Terraform / OpenTofu if:
Move to SST if:
Stay on Serverless Framework or SAM only if you're already there and not feeling pain.
The same system across three tools to make the difference concrete.
const vpc = new ec2.Vpc(this, "Vpc");
const db = new rds.DatabaseInstance(this, "Db", {
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_16 }),
vpc,
credentials: rds.Credentials.fromGeneratedSecret("admin"),
});
const queue = new sqs.Queue(this, "Queue");
const api = new apigw.RestApi(this, "Api");
// ...plus Lambda definitions, VPC config, IAM roles, and wiring
Plus a separate application program that reads env vars at runtime to find the DB and queue.
const db = new aws.rds.Instance("db", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
});
const queue = new aws.sqs.Queue("queue");
// ...plus Lambda and API Gateway, same ballpark as CDK
Same shape, different engine under the hood.
const db = new SQLDatabase("orders", { migrations: "./migrations" });
const queue = new Topic<OrderEvent>("order-events", {
deliveryGuarantee: "at-least-once",
});
export const placeOrder = api(
{ method: "POST", path: "/orders", expose: true },
async (req: PlaceOrder): Promise<Order> => {
const order = await db.queryRow`INSERT INTO orders ...`;
await queue.publish({ orderId: order.id });
return order;
},
);
That's the whole thing. The infrastructure and the application are the same program. There's no IAM wiring because Encore applies least-privilege automatically based on what each service accesses.
# Encore
brew install encoredev/tap/encore
encore app create my-app --example=ts/empty
cd my-app && encore run
# Pulumi
curl -fsSL https://get.pulumi.com | sh
pulumi new aws-typescript
# Terraform / OpenTofu
brew install opentofu
tofu init
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.