Deploying containers by hand (SSH, docker run) doesn't scale. ECS Fargate is AWS's managed container orchestration — define a task (container + CPU/memory), define a service (how many copies, load balancer, scaling policy), and Fargate handles provisioning, networking, and updates.
5 min read
You've built a Docker image, pushed it to ECR. Now deploy it:
ssh deploy@prod-server-1
docker pull my-app:latest
docker run -p 3000:3000 my-app:latestProblems: you have 1 running instance. If it crashes, the application is down. If you need 3 copies for redundancy, you SSH to 3 servers and repeat. If you need 100 copies, you're manually managing 100 containers. Updates mean stopping, pulling a new image, and restarting — with downtime if not coordinated.
Container orchestration (ECS, Kubernetes) automates this: you declare "run 3 copies of my app," and the orchestrator provisions containers, replaces failed ones, handles updates with zero downtime, and scales based on load.
ECS Task Definition: a blueprint for running a Docker container, specifying:
123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest)resource "aws_ecs_task_definition" "app" {
family = "my-app"
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.ecs_task.arn
container_definitions = jsonencode([{
name = "app"
image = "123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest"
portMappings = [{
containerPort = 3000
hostPort = 3000
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/my-app"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}])
}ECS Service: runs and maintains a desired number of task copies:
resource "aws_ecs_service" "app" {
name = "my-app-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 3 # run 3 copies
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.private_1.id, aws_subnet.private_2.id]
security_groups = [aws_security_group.app.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = 3000
}
depends_on = [aws_lb_listener.app]
}The service ensures 3 copies are always running — if one crashes, ECS launches a replacement automatically.
Push a new image with a new tag. Update the task definition to reference it. ECS performs a rolling update:
At every step, at least 1 task is running the old version (backward compatible with the load balancer), so requests don't drop.
resource "aws_ecs_service" "app" {
deployment_configuration {
maximum_percent = 200 # allow 6 tasks (3 old + 3 new) during update
minimum_healthy_percent = 100 # maintain at least 3 (100%) healthy
}
}Define scaling policies based on metrics (CPU, memory, request count):
resource "aws_appautoscaling_target" "ecs_target" {
max_capacity = 10
min_capacity = 3
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "ecs_policy_cpu" {
policy_name = "cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs_target.resource_id
scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs_target.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0 # scale to keep CPU at ~70%
}
}When CPU usage across tasks averages > 70%, ECS launches more tasks. When it drops below 70%, tasks are terminated. The service stays sized to load automatically.
Execution role: ECS uses this to pull images from ECR and write logs to CloudWatch — permissions ECS itself needs.
Task role: containers use this to access AWS services at runtime (S3, SQS, databases). Permissions your application needs.
resource "aws_iam_role" "ecs_execution" {
assume_role_policy = jsonencode({
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution_policy" {
role = aws_iam_role.ecs_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "ecs_task" {
assume_role_policy = jsonencode({
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "ecs_task_s3" {
role = aws_iam_role.ecs_task.id
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "arn:aws:s3:::my-bucket/*"
}]
})
}The application code (inside the container) assumes the task role and gets temporary credentials to access S3.
ECS Fargate is "serverless" — AWS manages the infrastructure. But you still need to monitor:
ECSServiceHealthCheck.deployments field.resource "aws_cloudwatch_metric_alarm" "ecs_unhealthy" {
alarm_name = "ecs-unhealthy-tasks"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 2
metric_name = "ECSServiceHealthCheck"
namespace = "AWS/ECS"
period = 60
statistic = "Average"
threshold = 0
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
ClusterName = aws_ecs_cluster.main.name
ServiceName = aws_ecs_service.app.name
}
}ECS Fargate (managed):
Kubernetes (self-managed or EKS):
For most applications on AWS, ECS Fargate is simpler and sufficient. The next lessons in this domain cover Kubernetes's own concepts directly, for when that flexibility is worth the added operational surface.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What is an ECS task definition?
2. How does ECS ensure zero-downtime deployments?
3. What permissions should the task role have, and why is it separate from the execution role?
4. How does Application Auto Scaling differ from manual scaling?
Docker & Containers