Updated Nov 5, 20268 min read

How to Deploy to AWS Without Terraform

Deploy a Go, Express or NestJS backend to your own AWS account with Postgres, queues, cron and secrets declared in application code.

To deploy a Go, Express or NestJS backend to AWS without Terraform, use infrastructure from code: declare the resources in application code and let a deploy system provision them. Encore reads those declarations, provisions Postgres, queues, cron jobs, secrets and object storage in your own AWS account, and deploys the app on push. OpenTofu, Pulumi and AWS CDK remain options when a platform team wants a separate IaC codebase.

This guide puts Encore first, then shows the same Postgres-plus-queue shape for Express, NestJS and Go, compares the main alternatives, and ends with when to keep Terraform.

How do you deploy to AWS without writing Terraform?

  1. Encore: infrastructure from code for TypeScript and Go, into your own AWS or GCP account, or self-hosted with Docker.
  2. AWS CDK: constructs in a real language, compiled to CloudFormation, AWS only.
  3. Pulumi: a separate IaC program in TypeScript, Python, Go and other languages.
  4. SST: app-focused components in sst.config.ts, deployed with Pulumi under the hood.
  5. OpenTofu: drop-in HCL fork of Terraform when you only want an open source license.
  6. SAM / Serverless Framework: Lambda-centric packaging, not a general backend path.

The ranking is for application teams that own the backend. A platform team that already runs a large Terraform estate may keep OpenTofu or Pulumi for shared networking and use Encore only for application resources.

How do the options compare?

ApproachWhere infra is definedSeparate state?CloudsBest fit
EncoreTyped declarations in app codeNo app-level state file you maintainYour own AWS or GCP, or self-host with DockerProduct backends with Postgres, queues, cron, secrets
AWS CDKSeparate CDK appCloudFormation stacksAWS onlyAWS-only platform teams
PulumiSeparate Pulumi programPulumi state backendMulti-cloudPlatform-owned IaC in a real language
SSTsst.config.tsPulumi state in your accountMainly AWS and CloudflareTypeScript full-stack apps
OpenTofu.tf filesState fileAny providerLicense swap off Terraform BSL
SAMtemplate.yamlCloudFormationAWS Lambda-focusedEvent-driven Lambda services

Why is Encore first for application backends?

Encore

Encore reads infrastructure declarations in your TypeScript or Go code with static analysis, runs them locally with encore run, and provisions the matching managed services when you deploy to your own AWS or GCP account. It works with any framework: keep NestJS or Express for structure, or mount Gin, Echo, Chi and net/http handlers in Go as raw endpoints.

Josef Sima, Engineering Director at Groupon, said on the customers page: "Encore is our foundation for all new development. What used to take days or weeks between developers and infra teams now takes minutes."

How do you get started?

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create orders --example=ts/hello-world$ cd orders$ encore secret set --type dev,local StripeKey$ encore run

Connect AWS in the Encore dashboard under Cloud, grant the provisioning IAM role the wizard creates, then push to the branch configured for an environment.

Instance sizes, networking and backups live in environment configuration, outside the application code.

How do you deploy Express to AWS without Terraform?

Encore works alongside Express. You can wrap an existing router in a raw endpoint while you move handlers over, or declare new APIs with Encore and add infrastructure next to them. A small orders service with Postgres and a queue looks like this:

import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { Topic, Subscription } from "encore.dev/pubsub"; import { secret } from "encore.dev/config"; const db = new SQLDatabase("orders", { migrations: "./migrations" }); const stripeKey = secret("StripeKey"); interface OrderPlaced { orderId: string; } export const orderPlaced = new Topic<OrderPlaced>("order-placed", { deliveryGuarantee: "at-least-once", }); const _ = new Subscription(orderPlaced, "fulfill", { handler: async (event) => { // fulfill using stripeKey() and the order row void event.orderId; void stripeKey; }, }); export const create = api( { expose: true, method: "POST", path: "/orders" }, async (req: { total: number }): Promise<{ id: string }> => { const row = await db.queryRow<{ id: string }>` INSERT INTO orders (total) VALUES (${req.total}) RETURNING id::text AS id`; await orderPlaced.publish({ orderId: row!.id }); return { id: row!.id }; }, );

On AWS that maps to RDS PostgreSQL, SNS with SQS and Secrets Manager, and Encore provisions an IAM role for the service. See the Express migration guide for a forklift path that keeps your existing router during the move.

How do you deploy NestJS to AWS without Terraform?

Keep Nest modules, providers and dependency injection, add encore.dev, mark the directory with encore.service.ts, start Nest as a standalone application context, and expose Nest services through Encore api endpoints. Declare the database and a cron job next to the Nest providers:

import { Service } from "encore.dev/service"; import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { CronJob } from "encore.dev/cron"; import { Bucket } from "encore.dev/storage/objects"; export default new Service("nest"); const db = new SQLDatabase("appDB", { migrations: "./migrations" }); const receipts = new Bucket("receipts", { versioned: false }); export const cleanup = api({}, async () => { await db.exec` DELETE FROM orders WHERE completed = true AND created_at < NOW() - INTERVAL '30 days'`; }); const _ = new CronJob("cleanup-orders", { title: "Clean up old orders", every: "24h", endpoint: cleanup, }); export const uploadReceipt = api( { expose: false, method: "POST", path: "/receipts/:orderId" }, async (req: { orderId: string; body: string }): Promise<{ key: string }> => { const key = `${req.orderId}.json`; await receipts.upload(key, Buffer.from(req.body)); return { key }; }, );

uploadReceipt stays private, so /receipts/:orderId is not a public route. placeOrder requires auth, creates the order id itself, and uploads a receipt built from that order through the generated client, the same service-to-service call Encore uses between services:

import { Service } from "encore.dev/service"; import { api } from "encore.dev/api"; import { nest } from "~encore/clients"; export default new Service("orders"); export const placeOrder = api( { expose: true, auth: true, method: "POST", path: "/orders" }, async (req: { total: number }): Promise<{ key: string }> => { const orderId = crypto.randomUUID(); const receipt = await nest.uploadReceipt({ orderId, body: JSON.stringify({ orderId, total: req.total }), }); return receipt; }, );

encore run starts Postgres locally and runs migrations. A deploy provisions RDS, an S3 bucket, and the services on Fargate or Kubernetes in your account. Full walkthrough: Deploy NestJS to AWS.

How do you deploy a Go backend to AWS without Terraform?

In Go the same shape uses sqldb, pubsub and cron packages:

package orders import ( "context" "encore.dev/cron" "encore.dev/pubsub" "encore.dev/storage/sqldb" ) var db = sqldb.NewDatabase("orders", sqldb.DatabaseConfig{ Migrations: "./migrations", }) type OrderPlaced struct { OrderID string } var OrderPlacedTopic = pubsub.NewTopic[*OrderPlaced]("order-placed", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) type CreateParams struct { Total int `json:"total"` } type CreateResponse struct { ID string `json:"id"` } //encore:api public method=POST path=/orders func Create(ctx context.Context, p *CreateParams) (*CreateResponse, error) { var id string err := db.QueryRow(ctx, ` INSERT INTO orders (total) VALUES ($1) RETURNING id::text `, p.Total).Scan(&id) if err != nil { return nil, err } if _, err := OrderPlacedTopic.Publish(ctx, &OrderPlaced{OrderID: id}); err != nil { return nil, err } return &CreateResponse{ID: id}, nil } //encore:api private func Cleanup(ctx context.Context) error { _, err := db.Exec(ctx, ` DELETE FROM orders WHERE completed = true AND created_at < NOW() - INTERVAL '30 days' `) return err } var _ = cron.NewJob("cleanup-orders", cron.JobConfig{ Title: "Clean up old orders", Every: 24 * cron.Hour, Endpoint: Cleanup, })

Existing Gin or Echo routers can mount as raw endpoints while you migrate service by service.

What gets provisioned in your own AWS account?

From the declarations above, a typical AWS environment includes:

  • Networking: VPC, subnets, security groups, load balancer with HTTPS
  • Compute: ECS Fargate or EKS, chosen per environment
  • Database: RDS PostgreSQL in a private subnet, migrations applied on deploy
  • Messaging: SNS topics with SQS subscriptions for Pub/Sub
  • Scheduling: cron jobs triggered by the Encore platform, with no extra AWS resources
  • Storage: S3 buckets for object storage
  • Secrets: values set with encore secret set, stored in AWS Secrets Manager and injected at runtime
  • IAM: Encore provisions an IAM role for the service

The resources are standard managed services in your account that you can see in the AWS console, not a vendor-hosted runtime for production.

Can Encore run next to existing Terraform?

Yes. Keep Terraform for shared and org-level infrastructure, and let Encore own the application resources it supports. The Encore Terraform provider exposes provisioned resources as data sources, and existing databases such as RDS can be connected rather than recreated. The migration guide walks through an incremental path.

How do you self-host instead of using the Encore platform?

encore build docker --config=infra-config.json orders:latest

You bring Postgres, Redis, Pub/Sub (SNS+SQS, GCP Pub/Sub or NSQ), S3-compatible storage and metrics. The image runs your services. It does not run migrations or trigger cron jobs. You apply migrations yourself and call cron endpoints from your own scheduler. Details: Build Docker images and configure infrastructure.

Which approach should you pick?

  • Pick Encore if you ship a Go, Express or NestJS backend and want infrastructure from code in your own AWS account, with local Postgres and queues that match production declarations.
  • Pick AWS CDK or Pulumi if a platform team owns a separate IaC program and application developers should not declare cloud resources.
  • Pick SST if you are building a TypeScript full-stack app on AWS or Cloudflare and want components in one config file.
  • Pick OpenTofu if you like Terraform and only need an open source license.
  • Pick SAM if the product is Lambda-first and the rest of the backend is small.

Frequently asked questions

How do I deploy a Go or Express backend to AWS without Terraform?

Use a tool that generates the AWS resources for you. Infrastructure from code tools read resource declarations in your application. AWS CDK, Pulumi and SST let you describe infrastructure in TypeScript or Go instead of HCL. SAM fits Lambda-first services.

Does Encore deploy into my own AWS account?

Yes. Encore provisions into your own AWS or GCP account through a role you grant and can revoke (on GCP, a service account). Resources stay in your account. You can also self-host with encore build docker and bring your own Postgres, Redis, Pub/Sub, storage and metrics.

When should I still use Terraform?

Keep Terraform or OpenTofu for org-level and shared infrastructure, such as account structure, custom networking, specialized AWS services and company-wide IAM boundaries. Application-level resources are the part that most often moves to a higher-level tool.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders