AWS ECS vs EKS: Choosing the Right Container Orchestrator

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 ope

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
      }
    }
  ]
}

ECS Service with Autoscaling (Terraform)#

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.

Contents