Intermediate
Introduction: Why ECS Service Connect Changes the Game
If you’ve been running microservices on Amazon ECS, you’ve likely felt the pain of wiring services together. The traditional approach involves creating an Application Load Balancer (ALB) or Network Load Balancer (NLB) for each internal service, configuring target groups, setting up health checks, managing DNS through AWS Cloud Map manually, and watching your AWS bill grow with each new load balancer at roughly $16–22/month per ALB before data processing charges.
Amazon ECS Service Connect, launched at re:Invent 2022, solves this by providing built-in service mesh capabilities directly within ECS. It handles service discovery, traffic routing, load balancing, and observability — all without requiring you to deploy and manage separate load balancers or manually configure AWS Cloud Map service instances.
Who should read this: DevOps engineers and backend developers running microservices on ECS (Fargate or EC2) who want to simplify inter-service communication, reduce infrastructure costs, and gain built-in observability. You should already be comfortable with ECS concepts like task definitions, services, and clusters.
Prerequisites
- An AWS account with permissions to manage ECS, Cloud Map, and IAM
- AWS CLI v2 installed and configured (v2.9.0+ for Service Connect support)
- Basic familiarity with ECS task definitions, services, and clusters
- Docker images pushed to Amazon ECR (or a public registry) for testing
- Optional: Terraform 1.3+ with the AWS provider 4.60+ if following the IaC examples
How ECS Service Connect Works Under the Hood
ECS Service Connect builds on two existing AWS technologies: AWS Cloud Map for service discovery and an Envoy proxy sidecar that gets injected into your tasks automatically. Understanding this architecture is essential for debugging and making informed configuration choices.
Here’s what happens when you enable Service Connect on an ECS service:
- ECS injects an Envoy sidecar container into each task. You don’t define this container — ECS manages it entirely. It uses the
public.ecr.aws/aws-appmesh/aws-appmesh-envoyimage under the hood. - AWS Cloud Map namespace is created or referenced. Service Connect uses Cloud Map HTTP namespaces (not DNS namespaces) to register service endpoints.
- Each task registers itself with Cloud Map via the Envoy proxy, which handles health checking and deregistration automatically.
- Client-side load balancing is performed by the Envoy proxy within each calling task. This is the key difference — there’s no centralized load balancer. The proxy in the caller’s task distributes requests across healthy instances of the target service.
- Metrics are emitted automatically to Amazon CloudWatch in the
AWS/ECS/ManagedScalingnamespace, giving you request counts, latency percentiles, and error rates without any instrumentation.
There are two Service Connect roles a service can assume:
| Role | Description | Use Case |
|---|---|---|
| Client only | The service can call other Service Connect services but doesn’t expose an endpoint itself | Frontend services, batch processors, API gateways that only make outbound calls to internal services |
| Client and Server | The service exposes an endpoint AND can call other Service Connect services | Backend microservices that both receive and make internal requests |
Step-by-Step Setup with AWS CLI
Let’s build a practical example: two services — an order-api that receives requests and calls a payment-service internally. No load balancers involved.
Step 1: Create the Cloud Map Namespace
Service Connect requires an AWS Cloud Map HTTP namespace. This is different from the DNS-based namespaces used by ECS Service Discovery (the older approach).
# Create an HTTP namespace for Service Connect
aws servicediscovery create-http-namespace \
--name production \
--description "Production service connect namespace"
# Note the OperationId from the output, then check its status
aws servicediscovery get-operation \
--operation-id
# Once complete, note the namespace ID (e.g., ns-abc123def456)
# You can also list namespaces to find it:
aws servicediscovery list-namespaces \
--filters Name=TYPE,Values=HTTP,Condition=EQ
Step 2: Create the ECS Cluster with Service Connect Defaults
# Create cluster with a default Service Connect namespace
aws ecs create-cluster \
--cluster-name microservices-prod \
--service-connect-defaults namespace=arn:aws:servicediscovery:us-east-1:123456789012:namespace/ns-abc123def456
# If the cluster already exists, update it:
aws ecs update-cluster \
--cluster microservices-prod \
--service-connect-defaults namespace=arn:aws:servicediscovery:us-east-1:123456789012:namespace/ns-abc123def456
Setting a default namespace on the cluster means individual services don’t need to specify it explicitly, though they can override it.
Step 3: Register Task Definitions
The task definition for the payment-service (which will be a server):
cat << 'EOF' > payment-task-def.json
{
"family": "payment-service",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "payment-app",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-service:latest",
"portMappings": [
{
"name": "payment-http",
"containerPort": 8080,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/payment-service",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
EOF
aws ecs register-task-definition --cli-input-json file://payment-task-def.json
Critical detail: The name field in portMappings and the appProtocol field are required for Service Connect. The port mapping name (payment-http) is what you’ll reference when configuring the service. The appProtocol can be http, http2, or grpc — this tells the Envoy proxy which protocol-specific features to enable (like HTTP-level metrics and retry logic).
Step 4: Create the Payment Service with Service Connect (Client-Server)
cat << 'EOF' > payment-service.json
{
"cluster": "microservices-prod",
"serviceName": "payment-service",
"taskDefinition": "payment-service",
"desiredCount": 2,
"launchType": "FARGATE",
"networkConfiguration": {
"awsvpcConfiguration": {
"subnets": ["subnet-0abc123", "subnet-0def456"],
"securityGroups": ["sg-0abc123"],
"assignPublicIp": "DISABLED"
}
},
"serviceConnectConfiguration": {
"enabled": true,
"services": [
{
"portName": "payment-http",
"discoveryName": "payment",
"clientAliases": [
{
"port": 80,
"dnsName": "payment"
}
]
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/service-connect-proxy",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "payment"
}
}
}
}
EOF
aws ecs create-service --cli-input-json file://payment-service.json
Let’s break down the serviceConnectConfiguration:
portName: Must match thenamefrom your task definition’s port mappingdiscoveryName: The name registered in Cloud Map. Defaults toportNameif omitted.clientAliases.dnsName: The hostname other services will use to reach this service. Inside any task with Service Connect enabled,http://payment:80will route to this service.clientAliases.port: The port the Envoy proxy listens on locally. This can differ from the container port — useful for presenting services on standard ports like 80.logConfiguration: Captures Envoy proxy logs. Always configure this — you’ll need it for debugging.
Step 5: Create the Order API Service (Client-Server)
Register the order-api task definition similarly (with its own port mapping named order-http), then create the service:
cat << 'EOF' > order-service.json
{
"cluster": "microservices-prod",
"serviceName": "order-api",
"taskDefinition": "order-api",
"desiredCount": 2,
"launchType": "FARGATE",
"networkConfiguration": {
"awsvpcConfiguration": {
"subnets": ["subnet-0abc123", "subnet-0def456"],
"securityGroups": ["sg-0abc123"],
"assignPublicIp": "DISABLED"
}
},
"serviceConnectConfiguration": {
"enabled": true,
"services": [
{
"portName": "order-http",
"clientAliases": [
{
"port": 80,
"dnsName": "order-api"
}
]
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/service-connect-proxy",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "order"
}
}
}
}
EOF
aws ecs create-service --cli-input-json file://order-service.json
Now, from within the order-api application code, you can call the payment service simply by making HTTP requests to http://payment:80. No DNS propagation delays, no load balancer DNS names, no service discovery client libraries needed.
Terraform Implementation for Production
For production environments, you’ll want this as infrastructure as code. Here’s a complete Terraform module:
# This is HCL (Terraform), not Python — using python highlighting for readability
resource "aws_service_discovery_http_namespace" "this" {
name = "production"
description = "Production ECS Service Connect namespace"
}
resource "aws_ecs_cluster" "this" {
name = "microservices-prod"
service_connect_defaults {
namespace = aws_service_discovery_http_namespace.this.arn
}
}
resource "aws_ecs_task_definition" "payment" {
family = "payment-service"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_execution.arn
container_definitions = jsonencode([
{
name = "payment-app"
image = "${aws_ecr_repository.payment.repository_url}:latest"
portMappings = [
{
name = "payment-http"
containerPort = 8080
protocol = "tcp"
appProtocol = "http"
}
]
essential = true
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/payment-service"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "ecs"
}
}
}
])
}
resource "aws_ecs_service" "payment" {
name = "payment-service"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.payment.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
service_connect_configuration {
enabled = true
service {
port_name = "payment-http"
discovery_name = "payment"
client_alias {
port = 80
dns_name = "payment"
}
}
log_configuration {
log_driver = "awslogs"
options = {
"awslogs-group" = "/ecs/service-connect-proxy"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "payment"
}
}
}
# Important: Service Connect modifies the task, so ignore changes
# to task_definition if you're using deployments that revise it
depends_on = [aws_service_discovery_http_namespace.this]
}
# Security group must allow traffic between tasks
resource "aws_security_group" "ecs_tasks" {
name_prefix = "ecs-tasks-"
vpc_id = var.vpc_id
# Allow all traffic within the security