Netflix runs thousands of microservices on AWS, and the tooling most teams reach for, from Spinnaker to its resilience libraries, came out of the work Netflix did to make that many services deployable at all. That scale is the exception. A team with eight or ten services usually does not need a service mesh or a bespoke delivery platform, and the interesting question is which of the standard AWS building blocks to use and where the hidden work sits once services have to find each other, share secrets, and deploy on their own schedules.
AWS gives you three realistic ways to run the services themselves, and the choice between them drives most of what follows. It helps to see the whole path first, from a container image through provisioning to a running service across environments, because the runtime you pick changes each step.
A deploy flow for a microservice on AWS: build a container image, push it to ECR, provision the runtime and its dependencies, then roll the service out across staging and production.
Every option comes down to where your container or function runs and how much of the surrounding machinery you operate yourself.
| Option | What it is | You manage | Best fit |
|---|---|---|---|
| ECS on Fargate | Containers on a serverless runtime, no cluster | Task definitions, networking, scaling rules | Most teams and most services |
| ECS on EC2 | Containers on instances you own | The above plus the EC2 fleet | Steady, high-throughput workloads where EC2 is cheaper |
| EKS | Managed Kubernetes | The Kubernetes layer, add-ons, upgrades | Orgs already committed to Kubernetes |
| Lambda | Functions triggered by events or HTTP | Almost nothing at the runtime level | Spiky, event-driven, or low-traffic services |
ECS on Fargate is where most teams should start, because there is no cluster of machines to patch or scale and it plugs directly into the AWS networking, IAM, and secrets services you will need regardless. You define a task, point it at an image, and Fargate runs it. ECS on EC2 trades that simplicity for cost control, since a busy service running around the clock can be cheaper on reserved instances than on Fargate, at the price of managing the fleet.
EKS is worth its overhead when an organization has already standardized on Kubernetes, wants portability across clouds, or leans on the Kubernetes ecosystem for service meshes, custom controllers, or Helm-based delivery. For a single team with a handful of services, running a Kubernetes control plane is usually more platform than the problem calls for. Lambda fits a different shape of service, one that runs in bursts or reacts to events, where paying only for execution time beats keeping a container warm. Many backends mix these, with steady request-serving services on Fargate and a few event handlers on Lambda.
Picking a runtime is the visible decision, but most of the work arrives after it, once several services have to operate as one system.
Service-to-service networking. A service that used to be an in-process function call becomes a network request across a VPC, with security groups deciding who can reach whom and an internal load balancer in the path. Getting the subnets, security groups, and target groups right for every pair of services that talk is fiddly, and a wrong rule shows up as a timeout rather than a clear error.
Service discovery. Services need to find each other without hardcoded addresses, since instances come and go. AWS Cloud Map assigns each service a stable DNS name, and ECS Service Connect handles the routing and load balancing between services. This has to be kept current as services are added, renamed, or split, which is easy to let drift.
Secrets. Each service needs database credentials and API keys, and those belong in AWS Secrets Manager or SSM Parameter Store rather than in the image or environment files checked into git. Each service's task role should read only the secrets it needs, which means writing and maintaining a set of narrow IAM policies rather than one broad role shared everywhere.
CI/CD per service. The point of microservices is independent deploys, so each service wants its own pipeline that builds an image, pushes it to Amazon ECR, and updates just that service. In a monorepo, path filters trigger only the pipelines whose code changed. The friction arrives with changes that span services, like a new field that the producer and consumer both need, where independent pipelines have to be sequenced so the two sides stay compatible during the rollout.
None of these is hard on its own, but they multiply with the number of services, and each one is a place where a small inconsistency between environments turns into an outage.
The mechanical core of deploying one service to ECS is short. Build the image, push it to ECR, and force a new deployment so the service picks up the image.
# Build and push the service image to ECR
docker build -t orders .
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker tag orders "$ECR_REGISTRY/orders:$GIT_SHA"
docker push "$ECR_REGISTRY/orders:$GIT_SHA"
# Roll the ECS service onto the new image
aws ecs update-service \
--cluster backend \
--service orders \
--force-new-deployment
This is one service. The template gets copied per service, and the task definition behind it carries the real configuration: the image, the IAM task role, the secrets to inject, the networking, and the CPU and memory. Multiply the surrounding task definitions, IAM policies, load balancer rules, and Cloud Map entries across a dozen services and the pipeline script stops being the hard part.
The networking, discovery, secrets, and per-service pipeline work above is configuration that describes infrastructure you already implied by writing the services. Encore takes the other route and lets you declare that infrastructure as typed objects in your application code, then provisions it into your own AWS account. A Postgres database, a Pub/Sub topic, a cron job, or a secret is a value you define next to the code that uses it, and calls between services are type-checked function calls through the ~encore/clients import rather than HTTP requests you wire up by hand. The services stay in one codebase that reads like a monolith while deploying as separate services on AWS, which keeps the boundaries without the deployment glue growing service by service.
// orders/orders.ts
import { api } from "encore.dev/api";
import { users } from "~encore/clients";
export const create = api(
{ method: "POST", path: "/orders", expose: true },
async (p: CreateParams): Promise<Order> => {
// A type-checked call to another service, not a manual HTTP request.
const user = await users.get({ id: p.userId });
return placeOrder(user, p.items);
},
);
From that code, Encore provisions the AWS resources the services declare, using RDS for Postgres, SNS and SQS for Pub/Sub, S3 for object storage, and Fargate to run the services, and it wires up service discovery, secrets, and distributed tracing across them. Each resource comes with sensible production defaults, and you can change the specifics, such as instance size or scaling, from the Encore dashboard when a service needs something different. The services deploy together into your own AWS account, and encore build docker produces a standard container image if you want to run it elsewhere. The work it removes is the hand-maintained glue between services, the networking rules, discovery entries, and per-service task definitions that grow with each service you add. If you already manage infrastructure with Terraform, Encore's Terraform provider exposes each provisioned resource as a data source, so you can reference an Encore database or topic from your existing Terraform configuration. A large system still involves real distributed-systems work like data consistency across services, but the boundary plumbing stops being something you keep in sync by hand. The TypeScript walkthrough builds a multi-service backend on this path from scratch.
For most teams, ECS on Fargate is the best starting point, because it runs containers without a cluster to manage and integrates with the AWS networking, IAM, and secrets tools you already need. Choose EKS when you have committed to Kubernetes across the org or need its ecosystem. Choose Lambda for spiky, event-driven, or low-traffic services. Match the runtime to the service rather than picking one for everything.
Use ECS with Fargate unless you have a specific reason for Kubernetes. ECS is simpler to operate, has no control plane to run, and covers most microservice workloads with less day-to-day overhead. EKS pays off when you already run Kubernetes elsewhere, want its portability across clouds, or depend on the Kubernetes ecosystem for things like custom controllers, service meshes, or Helm-based delivery.
They call each other over the network by resolving a target, then sending a request. AWS Cloud Map gives services a DNS name for discovery, and an internal Application Load Balancer or ECS Service Connect routes and balances traffic between them. Security groups and IAM control who can reach what. The work most teams underestimate is keeping these names, routes, and permissions in sync as services change.
Store them in AWS Secrets Manager or SSM Parameter Store and grant each service's task role read access only to the secrets it needs. Inject them at runtime through the task definition or fetch them at startup, so they never sit in the image or in source control. Rotate database credentials and API keys on a schedule, and avoid sharing one broad secret across every service.
No. Kubernetes is one way to run microservices, not a requirement. ECS on Fargate, AWS Lambda, and plain containers on Fargate all run multi-service backends without a Kubernetes cluster. Kubernetes earns its complexity for large orgs that want a common platform across teams and clouds, but a small or mid-sized team usually ships faster on a managed container service.
Give each service its own pipeline that builds a container image, pushes it to Amazon ECR, and updates the service to the new image, so services deploy independently. Trigger a service's pipeline only when its code changes, which a monorepo can do with path filters. The harder parts are keeping environments consistent and coordinating changes that span several services at once.