Posts

Automated ECS Cluster Scaling for Cost Optimization

How to automatically shut down and start up services and EC2 instances in an ECS cluster to reduce costs outside of business hours

Cloud cost management is crucial, especially for development or testing environments that don't need to run 24/7. The following Terraform code allows you to automate the process of shutting down an AWS ECS cluster and its services at night and turning them back on in the morning. This way, you can significantly lower costs by paying only for the resources you actually use.

The code below defines schedules for the Auto Scaling group of EC2 instances and for the individual ECS services.

# Define a local service map to easily manage them all at once.
# Update this map to point to your ECS service modules.
locals {
  services = {
    # Replace the names and references with your service modules
    frontend = module.service_my_app_frontend
    backend  = module.service_my_app_backend
    worker   = module.service_my_app_worker
  }
}

################################################################################
# SCHEDULED SHUTDOWN AND STARTUP OF THE CLUSTER (EC2 INSTANCES)
################################################################################

# This action shuts down EC2 instances (scales to 0) at 22:05.
resource "aws_autoscaling_schedule" "scale_down_ec2_cluster" {
  scheduled_action_name  = "scale-down-ecs-cluster-nightly"
  # Change the name below to your cluster's autoscaling group name.
  autoscaling_group_name = module.cluster.autoscaling_group.name
  recurrence             = "5 20 * * *" # Cron in UTC (e.g., 22:05 CEST is 20:05 UTC)
  min_size               = 0
  max_size               = 0
  desired_capacity       = 0
}

# This action turns on EC2 instances (scales to 1) at 05:55.
resource "aws_autoscaling_schedule" "scale_up_ec2_cluster" {
  scheduled_action_name  = "scale-up-ecs-cluster-daily"
  # Change the name below to your cluster's autoscaling group name.
  autoscaling_group_name = module.cluster.autoscaling_group.name
  recurrence             = "55 3 * * *" # Cron in UTC (e.g., 05:55 CEST is 03:55 UTC)
  min_size               = 1          # Set the target values for your environment.
  max_size               = 3
  desired_capacity       = 1
}


################################################################################
# SCHEDULED SHUTDOWN AND STARTUP OF ALL ECS SERVICES
################################################################################

# Register each service as a target for application auto-scaling.
resource "aws_appautoscaling_target" "ecs_service_targets" {
  for_each = local.services

  max_capacity       = 1
  min_capacity       = 1
  # Change 'module.cluster.name' if your cluster name comes from a different source.
  resource_id        = "service/${module.cluster.name}/${each.value.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

# Action to shut down all services (scaling to 0) at 22:00.
resource "aws_appautoscaling_scheduled_action" "scale_down_ecs_services" {
  for_each = local.services

  name               = "${each.key}-scale-down-nightly"
  service_namespace  = aws_appautoscaling_target.ecs_service_targets[each.key].service_namespace
  resource_id        = aws_appautoscaling_target.ecs_service_targets[each.key].resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service_targets[each.key].scalable_dimension
  schedule           = "cron(0 20 * * ? *)" # Cron in UTC (e.g., 22:00 CEST is 20:00 UTC)

  scalable_target_action {
    min_capacity = 0
    max_capacity = 0
  }
}

# Action to turn on all services (scaling to 1) at 06:00.
resource "aws_appautoscaling_scheduled_action" "scale_up_ecs_services" {
  for_each = local.services

  name               = "${each.key}-scale-up-daily"
  service_namespace  = aws_appautoscaling_target.ecs_service_targets[each.key].service_namespace
  resource_id        = aws_appautoscaling_target.ecs_service_targets[each.key].resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service_targets[each.key].scalable_dimension
  schedule           = "cron(0 4 * * ? *)" # Cron in UTC (e.g., 06:00 CEST is 04:00 UTC)

  scalable_target_action {
    min_capacity = 1
    max_capacity = 1
  }
}

Remember the Time Zone

All schedules (recurrence and schedule) in AWS are defined in the UTC time zone. Make sure you convert the hours to your local time zone, accounting for daylight saving time. The example shows conversions for Central European Summer Time (CEST = UTC+2).