AWS ECS Fargate: Serverless Container Deployment Without Managing Nodes
ECS vs EKS: When to Choose Fargate
Fargate removes EC2 node management from the equation — you define CPU and memory per task, and AWS handles the underlying compute. Choose ECS Fargate over EKS when:
- You have fewer than 50 services and don't need Kubernetes-native features
- Your team doesn't want to manage cluster upgrades, node groups, and Kubernetes complexity
- You're already deep in the AWS ecosystem (CodePipeline, App Mesh, CloudMap)
- You need serverless burst scaling with per-task billing
Choose EKS when you need Helm ecosystem, Kubernetes operators, multi-cloud portability, or advanced scheduling.
Core Concepts
- Cluster: A logical grouping of tasks. Just a namespace — Fargate clusters have no EC2 instances.
- Task Definition: Blueprint for a container — image, CPU, memory, environment variables, IAM role, logging.
- Task: A running instance of a task definition (like a pod).
- Service: Keeps N copies of a task running, integrates with load balancers, handles rolling deploys.
- Task Role: IAM role assumed by the running container (for AWS API access).
- Execution Role: IAM role used by ECS to pull images and write logs (not the container).
IAM Roles Setup
Two distinct roles are needed:
# 1. Task Execution Role — ECS agent uses this to pull ECR images and write to CloudWatch
aws iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# Add SSM/Secrets Manager access if using secrets injection
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite # scope this down in production
# 2. Task Role — your application assumes this to call AWS APIs
aws iam create-role \
--role-name myapp-task-role \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
# Attach only what the app needs (S3 read in this example)
aws iam attach-role-policy \
--role-name myapp-task-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Task Definition
A task definition is a JSON blueprint registered with ECS:
{
"family": "myapp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789:role/myapp-task-role",
"containerDefinitions": [
{
"name": "myapp",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{ "name": "APP_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "info" }
],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/myapp/db-password-AbCdEf"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:ssm:us-east-1:123456789:parameter/prod/myapp/api-key"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"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
},
"essential": true
}
]
}
Register it:
aws ecs register-task-definition \
--cli-input-json file://task-definition.json
Secrets from Secrets Manager and SSM
Never put secrets in environment — use secrets which pulls values at task start time and injects them as environment variables. The task execution role needs read access to the secret.
Secrets Manager:
# Create a secret
aws secretsmanager create-secret \
--name prod/myapp/db-password \
--secret-string "my-super-secure-password"
# Reference in task definition — use full ARN or name
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/myapp/db-password"
SSM Parameter Store (cheaper, simpler):
# Create a SecureString parameter
aws ssm put-parameter \
--name /prod/myapp/api-key \
--value "abc123-secret-key" \
--type SecureString
# Reference in task definition
"valueFrom": "arn:aws:ssm:us-east-1:123456789:parameter/prod/myapp/api-key"
# Or shorthand:
"valueFrom": "/prod/myapp/api-key"
ECS Service with Application Load Balancer
# Create the ECS cluster
aws ecs create-cluster --cluster-name production
# Create CloudWatch log group
aws logs create-log-group --log-group-name /ecs/myapp
# Create the service
aws ecs create-service \
--cluster production \
--service-name myapp \
--task-definition myapp:1 \
--desired-count 3 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-abc,subnet-def],securityGroups=[sg-xyz],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=myapp,containerPort=8080" \
--health-check-grace-period-seconds 60 \
--deployment-configuration "minimumHealthyPercent=100,maximumPercent=200"
minimumHealthyPercent=100 means rolling deploys keep all current tasks running while new ones start. maximumPercent=200 means ECS can run up to 2x the desired count during deployment.
Deploying New Versions (Rolling Update)
# Build and push to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
docker build -t myapp .
docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:${GIT_SHA}
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:${GIT_SHA}
# Register new task definition revision with updated image
aws ecs register-task-definition \
--cli-input-json "$(cat task-definition.json | sed "s|:latest|:${GIT_SHA}|g")"
# Update the service to use the new revision
aws ecs update-service \
--cluster production \
--service myapp \
--task-definition myapp:2 # or use the new revision number
# Wait for deployment to complete
aws ecs wait services-stable \
--cluster production \
--services myapp
Auto Scaling
ECS Application Auto Scaling adjusts the number of running tasks:
# Register the scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 \
--max-capacity 20
# Scale based on CPU usage
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--policy-name myapp-cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
# Also scale on ALB request count per target (better for HTTP services)
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--policy-name myapp-request-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 1000,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/myapp-alb/abc123/targetgroup/myapp-tg/xyz789"
}
}'
Terraform Example
resource "aws_ecs_cluster" "main" {
name = "production"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_task_definition" "myapp" {
family = "myapp"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.myapp_task.arn
container_definitions = jsonencode([{
name = "myapp"
image = "${aws_ecr_repository.myapp.repository_url}:latest"
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
secrets = [
{ name = "DB_PASSWORD", valueFrom = aws_secretsmanager_secret.db_password.arn }
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.myapp.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
essential = true
}])
}
resource "aws_ecs_service" "myapp" {
name = "myapp"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.myapp.arn
desired_count = 3
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.myapp.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.myapp.arn
container_name = "myapp"
container_port = 8080
}
deployment_circuit_breaker {
enable = true
rollback = true
}
lifecycle {
ignore_changes = [task_definition] # Let CI/CD manage image updates
}
}
Cost Breakdown
Fargate pricing is per vCPU-hour and GB-hour of memory:
- Linux/x86: $0.04048/vCPU-hour, $0.004445/GB-hour
- A 0.5 vCPU / 1 GB task running continuously: ~$16/month
- A 2 vCPU / 4 GB task: ~$65/month
Compared to an EC2 m5.large ($0.096/hour = ~$70/month) running at 30% utilization, Fargate can be cheaper if tasks are bursty. For steady high-utilization, EC2 + Savings Plans wins.
Fargate Spot: Up to 70% discount for fault-tolerant workloads. Specify capacityProviderStrategy with FARGATE_SPOT to use it for non-critical tasks.
Was this article helpful?
Senior Kubernetes Architect
10+ years orchestrating containers in production. Battle-tested opinions on everything from pod scheduling to service mesh. I've seen clusters burn and helped rebuild them better.
Related Articles
AWS EKS: Production Kubernetes Cluster Setup from Scratch
Step-by-step guide to launching a production-ready EKS cluster on AWS — node groups, IAM roles, VPC configuration, managed add-ons, kubeconfig setup, and cost optimization. Both eksctl and Terraform approaches covered.
AWS Core Services: The DevOps Engineer's Essential Guide
Navigate the essential AWS building blocks — EC2, S3, VPC, IAM, RDS, Lambda, and EKS explained for DevOps engineers with practical examples.
Alibaba Cloud ECS: Deploy and Manage Virtual Machines at Scale
Deploy and manage Alibaba Cloud ECS instances — instance families, storage types, security groups, auto-scaling groups, SLB load balancers, image management, and Terraform configuration for production workloads.
Google Cloud Run: Serverless Containers That Scale to Zero
Deploy and manage containerized workloads on Google Cloud Run — no infrastructure to manage. Covers service configuration, traffic splitting, secrets, VPC connectivity, Cloud Run Jobs, and cost optimization.
Fix AWS EC2 Instance 'Status Check Failed' Errors
Diagnose and recover AWS EC2 instances with failed system or instance status checks using stop/start, volume rescue, and auto-recovery techniques.
AWS Lambda Cold Start Optimization Using Provisioned Concurrency And SnapStart
Cold starts are the silent SLO killer in serverless architectures. You've built a beautiful Lambda function, deployed it, and everything looks great in you...
More in AWS
View all →Fix AWS S3 'Access Denied' Errors
Systematically troubleshoot and fix AWS S3 Access Denied errors caused by IAM policies, bucket policies, ACLs, and encryption settings.
AWS IAM Least Privilege: Policies That Won't Lock You Out
Build AWS IAM policies using least privilege without locking yourself out — practical patterns for roles, service accounts, and permission boundaries.
Cloud Landing Zone Architecture: Account Structure Done Right
How to design an AWS Landing Zone with Organizations, SCPs, and account vending that scales from 5 to 500 accounts without becoming a mess.
AWS CLI: Cheat Sheet
AWS CLI cheat sheet with copy-paste commands for EC2, S3, IAM, Lambda, ECS, CloudFormation, SSM, and Secrets Manager operations.