03/24/26

How to Set Up Microservices on AWS Without Terraform

Multi-service deployment without the infrastructure configuration

6 Min Read

Deploying a single service to AWS is manageable. Deploying five or ten services, each with its own database, networking rules, and scaling configuration, is where infrastructure tooling starts to dominate your week. Terraform is the default choice for this, but it's not the only one, and for microservices specifically, the configuration burden scales in ways that catch teams off guard.

This guide walks through three approaches to deploying microservices on AWS: ECS with Terraform, ECS with higher-level tools like AWS Copilot, and infrastructure-from-code with Encore.

Approach 1: ECS with Terraform

Terraform gives you full control over every AWS resource. For a single service, that's a reasonable amount of configuration. For microservices, the resource count multiplies per service.

Here's what you need for each service in your system:

  • ECS task definition: container image, CPU/memory limits, environment variables, logging configuration
  • ECS service definition: desired count, deployment strategy, health check grace period
  • Application Load Balancer target group: health check path, port, protocol, deregistration delay
  • ALB listener rule: path-based or host-based routing to the correct target group
  • Security groups: ingress/egress rules per service, allowing traffic from the load balancer and between services
  • Service discovery: Cloud Map namespace and service entries so services can find each other
  • IAM roles: task execution role (pulling images, writing logs) and task role (accessing other AWS services)
  • ECR repository: container registry for the service image

For a system with five services, you're looking at 40+ Terraform resources before you've added any databases, caches, or queues. Each service needs its own task definition file, and changes to shared infrastructure (VPC, ALB) need to account for all services simultaneously.

A typical project structure looks like this:

infra/ modules/ service/ # reusable module per service main.tf # task def, ECS service, target group variables.tf # service-specific config iam.tf # per-service roles outputs.tf environments/ staging/ main.tf # instantiate each service module vpc.tf # shared networking alb.tf # shared load balancer rds.tf # databases production/ # duplicate of staging with different variables

State management adds another layer. Most teams split state files per environment, sometimes per service, requiring remote backends and state locking via S3 and DynamoDB.

The result is functional and flexible, but you're spending significant time on infrastructure code that isn't your product.

Approach 2: AWS Copilot or CDK

Higher-level tools reduce the boilerplate by providing abstractions over the same ECS resources.

AWS Copilot (note: reaching end-of-support in June 2026, with AWS recommending migration to ECS Express Mode or CDK) lets you describe services in a manifest file:

# copilot/users/manifest.yml name: users type: Backend Service image: build: ./users/Dockerfile port: 8080 http: path: "/users" healthcheck: "/health" count: range: 1-5 cpu_percentage: 70 variables: DB_HOST: !Ref UsersDB

This is less configuration than raw Terraform. Copilot handles the ECS cluster, VPC, ALB, and service discovery setup. But you still manage Dockerfiles for each service, write manifest files per service, handle database provisioning separately, and configure service-to-service communication through environment variables or service discovery DNS names.

CDK offers a similar reduction in lines of code while keeping you in a general-purpose programming language (TypeScript, Python). The tradeoff is that you're still explicitly defining infrastructure resources. You've traded HCL for TypeScript, but the mental model is the same: you describe what AWS resources you want, and the tool provisions them.

For teams that need fine-grained control over AWS resources, Copilot and CDK are a real improvement over raw Terraform. The per-service overhead is lower, and the tooling handles more of the cross-cutting concerns. But you're still operating in infrastructure-land. Every new service still means new configuration, new deployment targets, and new things to keep in sync.

Approach 3: Infrastructure from Code with Encore

Infrastructure-from-code flips the model. Instead of writing infrastructure configuration and application code separately, you write application code that declares the resources it needs. The tooling figures out the infrastructure.

With Encore, each service is a directory with TypeScript files. APIs are defined with type-safe decorators, and infrastructure resources are declared as objects in your code:

// service: users/users.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("users", { migrations: "./migrations" }); export const get = api( { method: "GET", path: "/users/:id", expose: true }, async ({ id }: { id: string }) => { return db.queryRow`SELECT * FROM users WHERE id = ${id}`; } );
// service: orders/orders.ts import { api } from "encore.dev/api"; import { users } from "~encore/clients"; export const getOrderWithUser = api( { method: "GET", path: "/orders/:id", expose: true }, async ({ id }: { id: string }) => { const order = await getOrderFromDB(id); const user = await users.get({ id: order.userId }); return { ...order, user }; } );

That import { users } from "~encore/clients" line is doing a lot of work. Encore's compiler sees this, understands that the orders service depends on the users service, and generates a type-safe client. In production, this becomes an HTTP call with automatic retries, tracing, and structured logging. You don't configure service discovery, write HTTP clients, or manage API schemas.

The SQLDatabase declaration in the users service is equally significant. Encore reads it at build time and provisions a PostgreSQL database on AWS (RDS) with the correct security groups, subnet placement, and IAM permissions. No Terraform. No CloudFormation.

When you connect your app to Encore Cloud and push, the platform:

  1. Parses your code to build an infrastructure graph
  2. Provisions AWS resources in your own cloud account (ECS, RDS, ALB, VPCs, security groups, IAM roles)
  3. Configures networking between services automatically
  4. Sets up observability (tracing, metrics, logs) with no additional code
  5. Creates preview environments for each pull request

Adding a new service means creating a new directory with a TypeScript file. There are no manifest files, Dockerfiles, or Terraform modules to add.

What Each Approach Costs You

ECS + TerraformECS + Copilot/CDKEncore
New service setupTask def, service, target group, SG, IAM, ECRManifest file, DockerfileNew directory with .ts file
Database per service30+ lines of Terraform per DBSeparate addon confignew SQLDatabase(...) in code
Service-to-service callsManual HTTP clients, service discoveryDNS-based discovery, manual clientsAuto-generated type-safe clients
Environment parityDuplicate Terraform per envCopilot environment abstractionAutomatic per environment
ObservabilityCloudWatch config, X-Ray setupPartial (logs automatic, tracing manual)Built-in tracing, metrics, logs
DeploymentCI pipeline + terraform applycopilot deploygit push

When to Use What

Terraform makes sense when you have dedicated platform engineers, need fine-grained control over every AWS resource, or operate in a regulated environment with strict infrastructure audit requirements.

Copilot and CDK work well for teams that want to stay within the AWS ecosystem and need moderate customization but don't want to manage raw Terraform.

Encore fits teams that want to focus on building services and are willing to let the platform handle AWS provisioning. It's particularly effective for new projects where you haven't already invested in Terraform modules, and for teams without dedicated infrastructure engineers.

The underlying question is whether your infrastructure configuration is a product differentiator or overhead. For most microservice deployments, it's overhead.

Further Reading

Ready to escape the maze of complexity?

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