02/28/26

Encore vs. Terraform vs. AWS CDK vs. Pulumi vs. SST

Infrastructure from Code vs. Infrastructure as Code, compared

5 Min Read

There are now five credible ways to get a TypeScript backend running on AWS. Terraform and HCL. AWS CDK and TypeScript constructs. Pulumi and imperative TypeScript. SST and its serverless-first abstractions. And infrastructure from code, where the infrastructure is derived from the application itself.

Each makes a different trade-off between control, abstraction, and how much configuration you write. The differences are easier to see with a specific example than with feature matrices.

The Same Backend, Five Ways

The application: an orders service with a PostgreSQL database and a pub/sub topic. One service, one database, one event stream. Simple enough to compare, complex enough to show real infrastructure differences.

Terraform

# vpc.tf resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index) availability_zone = data.aws_availability_zones.available.names[count.index] } # rds.tf resource "aws_db_subnet_group" "orders" { name = "orders" subnet_ids = aws_subnet.private[*].id } resource "aws_db_instance" "orders" { identifier = "orders" engine = "postgres" engine_version = "15" instance_class = "db.t3.micro" allocated_storage = 20 db_name = "orders" username = "postgres" password = var.db_password db_subnet_group_name = aws_db_subnet_group.orders.name vpc_security_group_ids = [aws_security_group.rds.id] skip_final_snapshot = true backup_retention_period = 7 } # sns.tf resource "aws_sns_topic" "order_created" { name = "order-created" } resource "aws_sqs_queue" "order_created_dlq" { name = "order-created-dlq" } resource "aws_sqs_queue" "order_created" { name = "order-created" redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.order_created_dlq.arn maxReceiveCount = 3 }) } resource "aws_sns_topic_subscription" "order_created" { topic_arn = aws_sns_topic.order_created.arn protocol = "sqs" endpoint = aws_sqs_queue.order_created.arn } # ... plus security groups, ECS cluster, task definitions, # service definitions, load balancer, target groups, # ECR repository, CloudWatch log groups, IAM roles...

A production Terraform config for this setup is typically 300-500 lines across 6-10 files, plus a CI/CD pipeline to run terraform plan and terraform apply.

AWS CDK

const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 2 }); const db = new rds.DatabaseInstance(this, "OrdersDb", { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_15, }), instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO), vpc, databaseName: "orders", credentials: rds.Credentials.fromGeneratedSecret("postgres"), backupRetention: cdk.Duration.days(7), }); const topic = new sns.Topic(this, "OrderCreated"); const dlq = new sqs.Queue(this, "OrderCreatedDlq"); const queue = new sqs.Queue(this, "OrderCreatedQueue", { deadLetterQueue: { queue: dlq, maxReceiveCount: 3 }, }); topic.addSubscription(new subs.SqsSubscription(queue)); const cluster = new ecs.Cluster(this, "Cluster", { vpc }); const service = new ecs_patterns.ApplicationLoadBalancedFargateService( this, "OrdersService", { cluster, taskImageOptions: { image: ecs.ContainerImage.fromAsset("./orders"), environment: { DATABASE_URL: db.instanceEndpoint.socketAddress, SNS_TOPIC_ARN: topic.topicArn, }, }, } ); db.connections.allowFrom(service.service, ec2.Port.tcp(5432)); topic.grantPublish(service.taskDefinition.taskRole);

About 150 lines for the infrastructure. The application code is separate. Two codebases, one deploy pipeline per.

Pulumi

const vpc = new awsx.ec2.Vpc("vpc"); const db = new aws.rds.Instance("orders-db", { engine: "postgres", engineVersion: "15", instanceClass: "db.t3.micro", allocatedStorage: 20, dbName: "orders", username: "postgres", password: config.requireSecret("dbPassword"), dbSubnetGroupName: subnetGroup.name, vpcSecurityGroupIds: [sgRds.id], backupRetentionPeriod: 7, }); const topic = new aws.sns.Topic("order-created"); const dlq = new aws.sqs.Queue("order-created-dlq"); const queue = new aws.sqs.Queue("order-created-queue", { redrivePolicy: pulumi.jsonStringify({ deadLetterTargetArn: dlq.arn, maxReceiveCount: 3, }), });

Similar to CDK in scope. Imperative TypeScript instead of constructs, same separation between infrastructure and application code.

SST

const database = new sst.aws.Postgres("OrdersDb", { scaling: { min: "0.5 ACU", max: "2 ACU" }, }); const topic = new sst.aws.SnsTopic("OrderCreated"); topic.subscribe("notifications/subscriber.handler"); const api = new sst.aws.Function("OrdersApi", { handler: "orders/handler.handler", link: [database, topic], });

SST abstracts more than CDK or Pulumi. About 20 lines of infrastructure code. The application code is still separate, and you still manage the SST config and deployment pipeline.

Encore

// orders/orders.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { Topic } from "encore.dev/pubsub"; const db = new SQLDatabase("orders", { migrations: "./migrations" }); export const orderCreated = new Topic<OrderEvent>("order-created", { deliveryGuarantee: "at-least-once", }); export const create = api( { expose: true, method: "POST", path: "/orders" }, async (req: CreateOrderRequest): Promise<Order> => { const order = await db.queryRow<Order>` INSERT INTO orders (customer_id, total) VALUES (${req.customerId}, ${req.total}) RETURNING id, customer_id as "customerId", total`; await orderCreated.publish({ orderId: order!.id, customerId: req.customerId, total: req.total, }); return order!; } );

Zero lines of infrastructure code. The database, the pub/sub topic, and the API are declared in the application code. Encore.ts's compiler reads these declarations and builds an infrastructure graph. Encore Cloud provisions RDS, SNS+SQS, Fargate, IAM, and VPC from that graph, in your own AWS account.

Encore Cloud dashboard showing deployed services and infrastructure

Line Count Comparison

ApproachInfrastructure codeApplication codeConfig files
Terraform~400 lines HCL~60 lines TS8-12 .tf files + Dockerfile + CI/CD
AWS CDK~150 lines TS~60 lines TSCDK stack + Dockerfile + CI/CD
Pulumi~120 lines TS~60 lines TSPulumi program + Dockerfile + CI/CD
SST~20 lines TS~60 lines TSsst.config.ts + CI/CD
Encore0 lines~40 lines TSNone

The Workflow Difference Matters More

With Terraform, CDK, Pulumi, or SST, adding a database to your application is two changes: the application code that uses it, and the infrastructure code that provisions it. Two things to review, two things to keep in sync.

With infrastructure from code, adding a database is one change: const db = new SQLDatabase("orders", { migrations: "./migrations" }). One PR, one review, one deploy. This compounds with every infrastructure change over the lifetime of a project.

What You Give Up

Encore abstracts the resource-level configuration. You don't choose the RDS instance class, the VPC CIDR range, or the SQS visibility timeout. The platform handles those based on environment-level configuration that the infrastructure team sets once. For teams that need to tune specific parameters, Terraform or CDK gives you that control.

Encore also requires using its framework conventions: api() for endpoints, new SQLDatabase() for databases, new Topic() for pub/sub. You can generate Docker images with encore build docker and deploy anywhere if you outgrow the platform.

When to Use What

Terraform. When you need granular control over every resource across multiple cloud providers, or when your organization has invested in Terraform modules and expertise.

CDK / Pulumi. When you want infrastructure in a general-purpose language with access to every AWS resource, and your team is comfortable maintaining the infrastructure stack alongside the application.

SST. When you're building on AWS with a serverless-first approach and want higher-level abstractions without giving up access to underlying resources.

Encore. When the infrastructure configuration is the part you want to stop thinking about. When adding a service should be adding a directory, not adding a Terraform module.

Ready to escape the maze of complexity?

Encore Cloud is the development platform for building robust type-safe distributed systems with declarative infrastructure.