
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.
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:
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.
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.
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:
Adding a new service means creating a new directory with a TypeScript file. There are no manifest files, Dockerfiles, or Terraform modules to add.
| ECS + Terraform | ECS + Copilot/CDK | Encore | |
|---|---|---|---|
| New service setup | Task def, service, target group, SG, IAM, ECR | Manifest file, Dockerfile | New directory with .ts file |
| Database per service | 30+ lines of Terraform per DB | Separate addon config | new SQLDatabase(...) in code |
| Service-to-service calls | Manual HTTP clients, service discovery | DNS-based discovery, manual clients | Auto-generated type-safe clients |
| Environment parity | Duplicate Terraform per env | Copilot environment abstraction | Automatic per environment |
| Observability | CloudWatch config, X-Ray setup | Partial (logs automatic, tracing manual) | Built-in tracing, metrics, logs |
| Deployment | CI pipeline + terraform apply | copilot deploy | git push |
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.