DEV Community

Cover image for Fargate is Costing You 3x More: Switch to ECS on EC2 and Save Thousands with Terraform ๐Ÿ’ฐ
Suhas Mallesh
Suhas Mallesh

Posted on Edited on

Fargate is Costing You 3x More: Switch to ECS on EC2 and Save Thousands with Terraform ๐Ÿ’ฐ

AWS Fargate is convenient but expensive at scale. Hereโ€™s how to migrate to ECS on EC2 with Terraform and cut container costs by 60-70% without losing automation.

Fargate is amazing. Serverless containers, no infrastructure management, automatic scaling. Itโ€™s AWS at its finest.

Fargate is also expensive. Like, really expensive.

Hereโ€™s the math thatโ€™ll make you rethink your container strategy:

Running 10 containers (1 vCPU, 2GB RAM each):

Fargate cost:  $511/month
EC2 cost:      $164/month
Difference:    $347/month = $4,164/year

You're paying 3x more for convenience.
Enter fullscreen mode Exit fullscreen mode

For small workloads? Worth it. For production at scale? Youโ€™re hemorrhaging money.

Let me show you how to get Fargateโ€™s automation on EC2โ€™s pricing with Terraform.

๐Ÿ’ธ The Fargate Tax

Fargate pricing per task (1 vCPU, 2GB RAM):

  • vCPU: $0.04048/hour
  • Memory: 2GB ร— $0.004445/GB-hour = $0.00889/hour
  • Total: $0.04937/hour = $36/month per task

10 tasks running 24/7:

  • Cost: 10 ร— $36 = $360/month
  • Annual: $4,320

๐Ÿ’ฐ The EC2 Alternative

t3.xlarge instance (4 vCPU, 16GB RAM):

  • On-demand: $0.1664/hour = $121/month
  • 1-year Reserved: $0.1027/hour = $75/month
  • Capacity: Can run 10-12 containers easily

Running same 10 tasks on 2 ร— t3.xlarge:

  • Cost: 2 ร— $75 = $150/month (Reserved Instances)
  • Annual: $1,800

Savings: $2,520/year (58% reduction) ๐ŸŽ‰

And thatโ€™s conservative. With Spot Instances? $40/month (89% savings!).

๐Ÿ“Š When to Use What

Stick with Fargate:

โœ… Prototype/MVP stage (< 5 containers)

โœ… Unpredictable, bursty workloads

โœ… Team has zero DevOps capacity

โœ… Running <10 containers total

โœ… Cost isnโ€™t a primary concern

Switch to ECS on EC2:

โœ… Running 10+ containers consistently

โœ… Predictable traffic patterns

โœ… Cost optimization is a priority

โœ… You have basic infrastructure skills

โœ… Production workloads at scale

Break-even point: Around 8-10 containers running 24/7.

๐Ÿ› ๏ธ Terraform Implementation

Complete ECS on EC2 Setup

# modules/ecs-ec2-cluster/main.tf

# ECS Cluster
resource "aws_ecs_cluster" "main" {
  name = "production-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = {
    Name = "production-ecs-cluster"
  }
}

# Launch template for ECS instances
resource "aws_launch_template" "ecs" {
  name_prefix   = "ecs-instance-"
  image_id      = data.aws_ami.ecs_optimized.id
  instance_type = var.instance_type  # t3.xlarge

  iam_instance_profile {
    arn = aws_iam_instance_profile.ecs_instance.arn
  }

  vpc_security_group_ids = [aws_security_group.ecs_instances.id]

  user_data = base64encode(<<-EOF
    #!/bin/bash
    echo ECS_CLUSTER=${aws_ecs_cluster.main.name} >> /etc/ecs/ecs.config
    echo ECS_ENABLE_TASK_IAM_ROLE=true >> /etc/ecs/ecs.config
    echo ECS_ENABLE_CONTAINER_METADATA=true >> /etc/ecs/ecs.config
  EOF
  )

  monitoring {
    enabled = true
  }

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "ecs-instance"
    }
  }
}

# Get latest ECS-optimized AMI
data "aws_ami" "ecs_optimized" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"]
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "ecs" {
  name                = "ecs-asg"
  vpc_zone_identifier = var.private_subnet_ids
  min_size            = var.min_instances
  max_size            = var.max_instances
  desired_capacity    = var.desired_instances

  launch_template {
    id      = aws_launch_template.ecs.id
    version = "$Latest"
  }

  health_check_type         = "EC2"
  health_check_grace_period = 300

  tag {
    key                 = "Name"
    value               = "ecs-instance"
    propagate_at_launch = true
  }

  tag {
    key                 = "AmazonECSManaged"
    value               = "true"
    propagate_at_launch = true
  }
}

# Capacity provider for auto-scaling
resource "aws_ecs_capacity_provider" "main" {
  name = "capacity-provider"

  auto_scaling_group_provider {
    auto_scaling_group_arn         = aws_autoscaling_group.ecs.arn
    managed_termination_protection = "ENABLED"

    managed_scaling {
      maximum_scaling_step_size = 10
      minimum_scaling_step_size = 1
      status                    = "ENABLED"
      target_capacity           = 80  # Keep cluster at 80% utilization
    }
  }
}

resource "aws_ecs_cluster_capacity_providers" "main" {
  cluster_name = aws_ecs_cluster.main.name

  capacity_providers = [aws_ecs_capacity_provider.main.name]

  default_capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.main.name
    weight            = 100
  }
}

# IAM roles
resource "aws_iam_role" "ecs_instance" {
  name = "ecs-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_instance" {
  role       = aws_iam_role.ecs_instance.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_instance_profile" "ecs_instance" {
  name = "ecs-instance-profile"
  role = aws_iam_role.ecs_instance.name
}

# Security group
resource "aws_security_group" "ecs_instances" {
  name_prefix = "ecs-instances-"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 0
    to_port         = 65535
    protocol        = "tcp"
    security_groups = [var.alb_security_group_id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ecs-instances-sg"
  }
}

output "cluster_name" {
  value = aws_ecs_cluster.main.name
}

output "cluster_arn" {
  value = aws_ecs_cluster.main.arn
}
Enter fullscreen mode Exit fullscreen mode

Deploy a Service

# ecs-service.tf

resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  network_mode             = "bridge"  # Use bridge mode for EC2
  requires_compatibilities = ["EC2"]
  cpu                      = "512"     # Per task
  memory                   = "1024"    # Per task

  container_definitions = jsonencode([{
    name  = "app"
    image = "myapp:latest"

    portMappings = [{
      containerPort = 3000
      hostPort      = 0  # Dynamic port mapping
      protocol      = "tcp"
    }]

    environment = [
      { name = "NODE_ENV", value = "production" }
    ]

    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.app.name
        "awslogs-region"        = var.region
        "awslogs-stream-prefix" = "app"
      }
    }
  }])
}

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   = 10

  capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.main.name
    weight            = 100
  }

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = "app"
    container_port   = 3000
  }

  depends_on = [var.alb_listener]
}

resource "aws_cloudwatch_log_group" "app" {
  name              = "/ecs/my-app"
  retention_in_days = 7
}
Enter fullscreen mode Exit fullscreen mode

Add Spot Instances for Max Savings

# spot-instances.tf

resource "aws_launch_template" "ecs_spot" {
  name_prefix   = "ecs-spot-"
  image_id      = data.aws_ami.ecs_optimized.id
  instance_type = "t3.xlarge"

  iam_instance_profile {
    arn = aws_iam_instance_profile.ecs_instance.arn
  }

  vpc_security_group_ids = [aws_security_group.ecs_instances.id]

  # Request Spot instances
  instance_market_options {
    market_type = "spot"
    spot_options {
      max_price = "0.05"  # ~70% discount vs on-demand
    }
  }

  user_data = base64encode(<<-EOF
    #!/bin/bash
    echo ECS_CLUSTER=${aws_ecs_cluster.main.name} >> /etc/ecs/ecs.config
    echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config
  EOF
  )
}

resource "aws_autoscaling_group" "ecs_spot" {
  name                = "ecs-spot-asg"
  vpc_zone_identifier = var.private_subnet_ids
  min_size            = 1
  max_size            = 5
  desired_capacity    = 2

  launch_template {
    id      = aws_launch_template.ecs_spot.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "ecs-spot-instance"
    propagate_at_launch = true
  }
}

# Mix of on-demand and spot
resource "aws_ecs_capacity_provider" "spot" {
  name = "spot-capacity-provider"

  auto_scaling_group_provider {
    auto_scaling_group_arn = aws_autoscaling_group.ecs_spot.arn

    managed_scaling {
      status          = "ENABLED"
      target_capacity = 90
    }
  }
}

resource "aws_ecs_cluster_capacity_providers" "mixed" {
  cluster_name = aws_ecs_cluster.main.name

  capacity_providers = [
    aws_ecs_capacity_provider.main.name,    # On-demand
    aws_ecs_capacity_provider.spot.name,    # Spot
  ]

  # 70% spot, 30% on-demand for reliability
  default_capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.spot.name
    weight            = 70
  }

  default_capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.main.name
    weight            = 30
    base              = 2  # Always keep 2 on-demand instances
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ฐ Complete Cost Breakdown

Fargate (10 tasks, 1 vCPU, 2GB each)

Per task: $36/month
10 tasks: $360/month
Annual:   $4,320
Enter fullscreen mode Exit fullscreen mode

ECS on EC2 - On-Demand (2 ร— t3.xlarge)

2 ร— $121/month = $242/month
Annual:           $2,904
Savings:          $1,416/year (33%)
Enter fullscreen mode Exit fullscreen mode

ECS on EC2 - Reserved Instances (2 ร— t3.xlarge)

2 ร— $75/month = $150/month
Annual:         $1,800
Savings:        $2,520/year (58%)
Enter fullscreen mode Exit fullscreen mode

ECS on EC2 - Spot Instances (2 ร— t3.xlarge)

2 ร— $40/month = $80/month
Annual:         $960
Savings:        $3,360/year (78%)
Enter fullscreen mode Exit fullscreen mode

ECS on EC2 - Mixed (70% Spot, 30% RI)

1.4 ร— Spot ($40)  + 0.6 ร— RI ($75)  = $101/month
Annual:                                $1,212
Savings:                               $3,108/year (72%) ๐ŸŽ‰
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ Migration Strategy

Step 1: Deploy ECS Cluster (No Risk)

terraform apply -target=module.ecs_cluster
# Just creates the cluster, no migration yet
Enter fullscreen mode Exit fullscreen mode

Step 2: Migrate One Service

# Deploy task definition for EC2
terraform apply -target=aws_ecs_task_definition.app_ec2

# Create service with 0 tasks
terraform apply -target=aws_ecs_service.app_ec2

# Gradually shift traffic
# - Start EC2 service with 1 task
# - Test thoroughly
# - Scale up EC2, scale down Fargate
# - Complete cutover
Enter fullscreen mode Exit fullscreen mode

Step 3: Monitor Cost Impact

# Watch AWS Cost Explorer
# Compare week-over-week ECS costs
# Should see 50-70% reduction
Enter fullscreen mode Exit fullscreen mode

Step 4: Migrate Remaining Services

# Repeat for other services
# Keep mission-critical services on Fargate if needed
Enter fullscreen mode Exit fullscreen mode

โšก Quick Comparison Table

Factor Fargate ECS on EC2 (RI) ECS on EC2 (Spot)
10 tasks cost $360/mo $150/mo $80/mo
Setup complexity Low Medium Medium
Management overhead None Low Low-Medium
Auto-scaling Built-in Manual setup Manual setup
Cold start None None None
Reliability Very High High Medium-High
Best for <10 containers Steady workloads Cost-critical

๐Ÿ’ก Pro Tips

1. Use ECS Capacity Provider Auto-Scaling

Let AWS manage your EC2 fleet automatically:

managed_scaling {
  status          = "ENABLED"
  target_capacity = 80  # Keep 80% utilized
}
Enter fullscreen mode Exit fullscreen mode

This gives you Fargate-like hands-off experience on EC2.

2. Mix Instance Types

Use Spot for batch jobs, RI for critical services:

default_capacity_provider_strategy {
  capacity_provider = "SPOT"
  weight            = 80
  base              = 0
}

default_capacity_provider_strategy {
  capacity_provider = "ON_DEMAND"
  weight            = 20
  base              = 2  # Always 2 on-demand
}
Enter fullscreen mode Exit fullscreen mode

3. Right-Size Your Instances

Donโ€™t over-provision. Use CloudWatch to find optimal size:

# Check CPU utilization
aws cloudwatch get-metric-statistics \
  --namespace AWS/ECS \
  --metric-name CPUUtilization \
  --dimensions Name=ClusterName,Value=production-cluster \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-31T23:59:59Z \
  --period 86400 \
  --statistics Average
Enter fullscreen mode Exit fullscreen mode

If average CPU < 40%, downsize instances.

4. Enable Container Insights

Track per-service metrics even on EC2:

setting {
  name  = "containerInsights"
  value = "enabled"
}
Enter fullscreen mode Exit fullscreen mode

Worth the $2/month for visibility.

โš ๏ธ When NOT to Migrate

Keep Fargate for:

  • Prototyping - Speed over cost
  • Bursty workloads - Scale to zero capability
  • Small scale - <5 containers isnโ€™t worth the complexity
  • Compliance - Some regulations require no server management
  • Ultra-critical - Fargateโ€™s reliability is unmatched

๐ŸŽ“ Real-World Success Story

Startup running microservices:

Before (Fargate):

  • 25 services ร— 2 tasks each = 50 tasks
  • Average: 1 vCPU, 2GB per task
  • Monthly cost: 50 ร— $36 = $1,800/month

After (ECS on EC2 with mixed Spot/RI):

  • 4 ร— t3.xlarge instances (2 RI, 2 Spot)
  • 2 ร— $75 (RI) + 2 ร— $40 (Spot) = $230/month

Annual savings: $18,840 ๐Ÿ’ฐ

Migration time: 2 weeks

Team size: 2 engineers

Complexity added: Minimal (capacity provider handles scaling)

๐ŸŽฏ Summary

Fargate is amazing for prototypes and small workloads.

But once youโ€™re running 10+ containers 24/7, youโ€™re leaving money on the table.

The math is simple:

  • Fargate: $360/month for 10 tasks
  • ECS on EC2 (RI): $150/month
  • ECS on EC2 (Spot): $80/month

Pick your savings level:

  • Conservative (RI): 58% savings
  • Aggressive (Spot): 78% savings
  • Balanced (Mix): 72% savings

With Terraform and ECS capacity providers, you get 90% of Fargateโ€™s convenience at 30% of the cost.

Stop overpaying for containers. Your CFO will thank you. ๐Ÿš€


Migrated from Fargate to ECS on EC2? How much are you saving? Share in the comments! ๐Ÿ’ฌ

Follow for more AWS cost optimization with Terraform! โšก

Top comments (0)