Introduction#
ECS (Elastic Container Service) and EKS (Elastic Kubernetes Service) both run containerized workloads on AWS, but they represent fundamentally different philosophies. ECS is AWS-native, simpler to operate, and deeply integrated with IAM, ALB, and CloudWatch. EKS is managed Kubernetes — portable, extensible, and with a larger ecosystem, but significantly more complex to operate.
Architecture Comparison#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ECS Architecture:
Task Definition → Service → Cluster
Launch types: EC2 (your instances) or Fargate (serverless)
Networking: awsvpc mode (each task gets its own ENI)
Service discovery: AWS Cloud Map or ALB
EKS Architecture:
Pod → Deployment → Service → Ingress → Cluster
Node types: EC2 (managed node groups) or Fargate profiles
Networking: VPC CNI plugin
Service discovery: Kubernetes DNS (CoreDNS)
Ingress: AWS Load Balancer Controller
Key difference:
ECS: AWS manages the control plane AND scheduling
EKS: AWS manages only the Kubernetes control plane
You manage node groups, add-ons, and k8s config
|
ECS Task Definition#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| {
"family": "api-service",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123:role/apiTaskRole",
"containerDefinitions": [
{
"name": "api",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/api:latest",
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
"environment": [
{"name": "ENVIRONMENT", "value": "production"}
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:prod/db-url"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/api-service",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| resource "aws_ecs_service" "api" {
name = "api-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.api.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
deployment_circuit_breaker {
enable = true
rollback = true # auto-rollback on deployment failure
}
lifecycle {
ignore_changes = [desired_count] # managed by autoscaling
}
}
resource "aws_appautoscaling_target" "api" {
max_capacity = 20
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "api_cpu" {
name = "api-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.api.resource_id
scalable_dimension = aws_appautoscaling_target.api.scalable_dimension
service_namespace = aws_appautoscaling_target.api.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
|
EKS Deployment#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| # api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
serviceAccountName: api-sa # for IRSA (IAM Roles for Service Accounts)
containers:
- name: api
image: 123456789.dkr.ecr.us-east-1.amazonaws.com/api:latest
ports:
- containerPort: 8080
env:
- name: ENVIRONMENT
value: production
envFrom:
- secretRef:
name: api-secrets # from AWS Secrets Manager via External Secrets Operator
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123:certificate/abc
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
|
Feature Comparison#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Feature | ECS (Fargate) | EKS
-------------------------|----------------------------|----------------------
Setup complexity | Low | High
Operational overhead | Low | High
Control plane cost | Free | $0.10/hr (~$73/mo)
IAM integration | Native (task roles) | IRSA (additional setup)
Scaling | App Auto Scaling | HPA, KEDA, Karpenter
Service discovery | Cloud Map + ALB | CoreDNS + AWS LB Ctrl
Secrets management | Native Secrets Manager | External Secrets Operator
Multi-cloud portability | No (AWS-only) | Yes (same k8s manifests)
Ecosystem/extensions | Limited | Vast (Helm, operators)
Spot/Preemptible nodes | Fargate Spot | Spot instances + Karpenter
Job scheduling | ECS Scheduled Tasks | Kubernetes CronJobs
Node-level access | None (Fargate) | SSH to nodes
GPU workloads | Limited | Full support
|
Decision Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| Choose ECS when:
- Small-to-medium team without Kubernetes expertise
- You want AWS-managed everything with minimal ops overhead
- Fargate's serverless model simplifies capacity planning
- Your workloads are straightforward web services
- AWS lock-in is acceptable
Choose EKS when:
- You already have Kubernetes expertise
- Multi-cloud or hybrid cloud is a requirement
- You need Kubernetes-specific tooling (Istio, Argo, Karpenter)
- Complex workloads: ML training, stateful services, CRDs
- Large team where Kubernetes skills are available
- You need fine-grained node control (GPU types, spot strategies)
Start with ECS if:
- You're starting fresh and need something running fast
- You can always migrate to EKS later if you outgrow ECS
|
Conclusion#
ECS is the pragmatic choice for teams that want to run containers on AWS without becoming Kubernetes experts. It handles the common cases well with less operational surface area. EKS is the right choice when you need the Kubernetes ecosystem, portability across cloud providers, or advanced scheduling capabilities. The control plane cost ($73/month) is negligible for organizations that need what Kubernetes offers, but represents real overhead for small teams running a handful of services. Neither is universally better — the right choice depends on your team’s skills and operational requirements.