AWS for DevOps — ECS + ALB
7.5 — ECS + Application Load Balancer
An Application Load Balancer (ALB) distributes traffic to your ECS tasks. It provides health checking, path-based routing, and the ability to run multiple services behind a single domain.
Architecture
Users
│
▼
ALB (port 443)
┌─────────────────────────────────────────┐
│ Listener Rule: /api/* → TG: api-tg │
│ Listener Rule: /web/* → TG: web-tg │
│ Default → TG: default-tg│
└─────────────────────────────────────────┘
│ │
▼ ▼
api-tg web-tg
┌──────┐ ┌──────┐
│Task 1│ │Task 1│
│Task 2│ │Task 2│
│Task 3│ │Task 3│
└──────┘ └──────┘
ECS Service: api ECS Service: web
Step-by-Step Setup
1. Create the ALB
$ aws elbv2 create-load-balancer \
--name my-app-alb \
--subnets subnet-pub-a subnet-pub-b \
--security-groups sg-alb-123 \
--scheme internet-facing \
--type application
{
"LoadBalancers": [{
"LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-south-1:123456789012:loadbalancer/app/my-app-alb/abc123",
"DNSName": "my-app-alb-123456789.ap-south-1.elb.amazonaws.com"
}]
}
2. Create a Target Group
$ aws elbv2 create-target-group \
--name api-tg \
--protocol HTTP \
--port 8080 \
--vpc-id vpc-abc123 \
--target-type ip \
--health-check-path /health \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3
Important: For Fargate,
target-typemust beip(notinstance). Each task gets its own IP.
3. Create a Listener
# HTTP listener (redirect to HTTPS in production)
$ aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/my-app-alb/abc123 \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api-tg/def456
4. Create ECS Service with ALB
$ aws ecs create-service \
--cluster production \
--service-name api \
--task-definition api:3 \
--desired-count 3 \
--launch-type FARGATE \
--network-configuration \
"awsvpcConfiguration={subnets=[subnet-priv-a,subnet-priv-b],securityGroups=[sg-tasks-789],assignPublicIp=DISABLED}" \
--load-balancers \
"targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api-tg/def456,containerName=app,containerPort=8080" \
--health-check-grace-period-seconds 120
| Parameter | Why |
|---|---|
assignPublicIp=DISABLED |
Tasks are in private subnets — ALB handles public traffic |
containerName |
Must match the container name in your task definition |
containerPort |
Must match the portMappings in your task definition |
health-check-grace-period-seconds |
Give the app time to start before ALB marks it unhealthy |
Path-Based Routing
Route different URL paths to different ECS services:
# Rule 1: /api/* → api service
$ aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:...:listener/app/my-app-alb/abc123/def456 \
--priority 10 \
--conditions Field=path-pattern,Values='/api/*' \
--actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api-tg/...
# Rule 2: /admin/* → admin service
$ aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:...:listener/app/my-app-alb/abc123/def456 \
--priority 20 \
--conditions Field=path-pattern,Values='/admin/*' \
--actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/admin-tg/...
Host-Based Routing
Route different domains to different services:
$ aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:...:listener/... \
--priority 5 \
--conditions Field=host-header,Values='api.example.com' \
--actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api-tg/...
Health Checks
| Setting | Recommended Value | Why |
|---|---|---|
| Path | /health or /healthz |
Dedicated lightweight endpoint |
| Interval | 30 seconds |
Balance between detection speed and load |
| Healthy threshold | 2 |
2 consecutive successes = healthy |
| Unhealthy threshold | 3 |
3 consecutive failures = unhealthy |
| Timeout | 5 seconds |
Fail fast |
| Matcher | 200 |
Only 200 OK means healthy |
Security Group Rules
ALB Security Group (sg-alb) Task Security Group (sg-tasks)
┌────────────────────────┐ ┌──────────────────────────┐
│ Inbound: │ │ Inbound: │
│ TCP 80 from 0.0.0.0 │ │ TCP 8080 from sg-alb │
│ TCP 443 from 0.0.0.0 │ │ (ONLY from ALB!) │
│ │ │ │
│ Outbound: │ │ Outbound: │
│ TCP 8080 to sg-tasks │ │ All traffic (outbound) │
└────────────────────────┘ └──────────────────────────┘
Tasks should only accept traffic from the ALB security group — never directly from the internet.
Scaling Based on ALB Metrics
$ aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/production/api \
--policy-name request-count-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 500.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/my-app-alb/abc123/targetgroup/api-tg/def456"
}
}'
This scales the service to maintain approximately 500 requests per target.
Common Mistakes
| Mistake | Why It Hurts | Fix |
|---|---|---|
target-type=instance with Fargate |
Registration fails | Use target-type=ip for Fargate |
No health-check-grace-period |
New tasks killed before app starts | Set to 60-120 seconds |
| Tasks in public subnets | Unnecessary exposure | Put tasks in private subnets; ALB handles public traffic |
| ALB SG allows all ports to tasks | Wide open attack surface | Allow only the container port from ALB SG |
Quick Reference
| Task | Command |
|---|---|
| Create ALB | aws elbv2 create-load-balancer --name NAME --subnets ... --type application |
| Create target group | aws elbv2 create-target-group --name NAME --target-type ip --port PORT |
| Create listener | aws elbv2 create-listener --load-balancer-arn ARN --port 80 --default-actions ... |
| Add routing rule | aws elbv2 create-rule --listener-arn ARN --conditions ... --actions ... |
| Check target health | aws elbv2 describe-target-health --target-group-arn ARN |