What to use when CloudFormation limits and construct abstractions get in the way
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 | Application model and Encore-managed deployment state | Pulumi engine (no CloudFormation) | Terraform state | CDK → CloudFormation | CloudFormation | CloudFormation |
| Infrastructure config | Resource declarations in application code; deployment settings per environment | Separate program | Separate HCL files | Separate config | Separate YAML | Separate YAML |
| State management | Managed by Encore for platform-managed resources | Managed (Pulumi Cloud) or self-managed | Remote state file | CloudFormation stacks | CloudFormation stacks | CloudFormation stacks |
| 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 | Backend infrastructure derived from application code | Multi-cloud, CDK-like DX | Teams with existing Terraform estate | Serverless on AWS | Lambda-centric apps | AWS-native serverless |
Encore is relevant when a team wants the backend to declare the infrastructure it uses instead of maintaining a separate program that generates CloudFormation. Its open source Infra SDK for TypeScript and Go supplies the API, service, and infrastructure declarations that Encore reads into an application model.
An Encore declaration records a logical resource used by the application; it does not generate a CloudFormation resource. When deploying with Encore, supported resources are provisioned in AWS or GCP and their deployment settings are configured per environment. CDK can remain in place for shared or unsupported AWS infrastructure.
For teams using AI-first development workflows, the model gives agents direct context about APIs and resource usage. Type safety and static analysis catch invalid declarations during the build, and deployment-specific settings such as sizing, networking, and backups stay outside the application code agents edit.
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" });
// SNS topic on AWS, Pub/Sub topic on GCP.
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;
},
);
The declarations above are part of the running backend and tell Encore which services use each resource. CDK constructs belong to a separate application that synthesizes and deploys CloudFormation stacks. Existing CDK stacks stay under CDK ownership unless a resource is handed over through a supported import workflow.
Compute, capacity, networking, backups, and regions are configured per environment. CDK remains responsible for every construct and CloudFormation stack outside Encore's application model.
Application and infrastructure changes stay together. A database declaration sits in the service that uses it without carrying subnets, security groups, and instance configuration into the backend source.
There is no CloudFormation stack for the application team to maintain. Encore manages the deployment state required for supported resources it provisions, including resource dependencies and updates.
Agent changes are checked against the application model. Resource declarations are type-checked and must be statically discoverable, so invalid changes fail during the build. The MCP server supplies schemas, traces, and the service graph as additional context.
Local, preview, and cloud environments use the same declarations. The SDK supplies local infrastructure during development, and preview environments include the resources declared by the application.
Observability follows the application structure. Traces, logs, metrics, and service relationships are available without adding constructs for each signal and service.
Infrastructure whose topology is generated during synthesis and AWS resources without an Encore primitive can remain in CDK. Encore does not adopt a CDK construct tree or CloudFormation stack; connecting an existing resource uses an explicit, resource-specific ownership workflow.
CDK can continue to manage account configuration, VPCs, DNS, shared infrastructure, custom resources, and unsupported AWS services. Encore services connect to those resources through their normal SDKs, or a self-hosted environment maps Encore declarations to CDK-provisioned infrastructure.
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
Ship without waiting on Terraform
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.