Code Snip: This sites ECS CLuster

Friday, Jan 30, 2026

Code Snip: The Terraform configuration the ECS Cluster

Part of the Terraform for this site This Website

resource "aws_ecs_cluster" "server_cluster" {
  name = "website_server_cluster"

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

resource "aws_cloudwatch_log_group" "cluster_logs" {
  for_each = var.backend_tasks

  name = "/ecs/webserver/${each.key}"
  retention_in_days = 30
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name = "website_server_task_execution_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
  role = aws_iam_role.ecs_task_execution_role.name
}

resource "aws_iam_role" "ecs_task_role" {
  name = "website_server_task_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "assetaccess" {
  name   = "assetaccess"
  role   = aws_iam_role.ecs_task_role.name
  policy = file("s3files.json")
}


resource "aws_ecs_task_definition" "site_definitions" {
  for_each = var.backend_tasks

  family = "${each.key}_task"
  network_mode = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn = aws_iam_role.ecs_task_role.arn
  cpu = tostring(each.value.cpu)
  memory = tostring(each.value.memory)

  container_definitions = jsonencode([
    {
      name      = "${each.key}_container_task"
      image     = "${aws_ecr_repository.site_containers[each.key].repository_url}:latest"
      essential = true

      readonlyRootFilesystem = false # I'll need to check this out later. The app couldn't write to cache when this was on.

      healthCheck = {
        command = ["CMD-SHELL", "curl -f http://localhost:${each.value.internal_port}/ || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }

      portMappings = [
        {
          containerPort = each.value.internal_port
          # hostPort = 0
          protocol = "tcp"
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.cluster_logs[each.key].name
          "awslogs-stream-prefix" = "ecs"
          "awslogs-region"        = aws_vpc.ecs_vpc.region
        }
      }

        environment = [
          {
            name  = "PORT"
            value = tostring(each.value.internal_port)
          },
          {
            name  = "NEXT_PUBLIC_SITE_URL"
            value = "https://${aws_lb.ECSWebServerLB.dns_name}"
          },
          {
            name  = "INTERNAL_API_URL"
            value = "http://localhost:${each.value.internal_port}"
          },
          {
            name  = "HOSTNAME"
            value = "0.0.0.0"
          },
        ]
    }]
  )
}


resource "aws_ecs_service" "server_services" {
  for_each = var.backend_tasks

  name = "${each.key}_service"
  cluster = aws_ecs_cluster.server_cluster.id
  task_definition = aws_ecs_task_definition.site_definitions[each.key].arn
  desired_count = each.value.desired_count

  force_new_deployment = true

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight = 99
    base = 1
  }

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight = 1
    base = 0
  }

  network_configuration {
    subnets          = [for s in aws_subnet.private_subnets : s.id]
    security_groups  = [aws_security_group.ecs_fargate_sg.id]
    assign_public_ip = false  # Tasks in private subnets
  }

  dynamic "load_balancer" {
    for_each = {for k, v in var.frontend_apps : k => v if v.backend_key == each.key }

    content {
      target_group_arn = aws_lb_target_group.TG[load_balancer.key].arn
      container_name   = "${each.key}_container_task"
      container_port   = each.value.external_port
    }
  }

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  depends_on = [
    aws_lb_listener.ECSWebServerListener,
    aws_iam_role_policy_attachment.ecs_task_execution_role_policy,
  ]

  lifecycle {
    replace_triggered_by = [aws_subnet.private_subnets]
    create_before_destroy = true
  }
}

resource "aws_ecs_cluster_capacity_providers" "fargate" {
  cluster_name = aws_ecs_cluster.server_cluster.name

  capacity_providers = ["FARGATE", "FARGATE_SPOT"]

  default_capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 10
    base              = 1
  }
}

resource "aws_appautoscaling_target" "ecs_service" {
  for_each = var.backend_tasks

  max_capacity = 10
  min_capacity = 1
  resource_id = "service/${aws_ecs_cluster.server_cluster.name}/${aws_ecs_service.server_services[each.key].name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace = "ecs"
}

resource "aws_appautoscaling_policy" "ecs_service_cpu" {
  for_each = var.backend_tasks

  name = "${each.key}_cpu_scaling_policy"
  resource_id = aws_appautoscaling_target.ecs_service[each.key].resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service[each.key].scalable_dimension
  service_namespace = aws_appautoscaling_target.ecs_service[each.key].service_namespace
  policy_type = "TargetTrackingScaling"

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }

    target_value = 75.0
    scale_in_cooldown = 300
    scale_out_cooldown = 60
  }
}

resource "aws_appautoscaling_policy" "ecs_service_memory" {
  for_each = var.backend_tasks

  name = "${each.key}_memory_scaling_policy"
  resource_id = aws_appautoscaling_target.ecs_service[each.key].resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service[each.key].scalable_dimension
  service_namespace = aws_appautoscaling_target.ecs_service[each.key].service_namespace
  policy_type = "TargetTrackingScaling"

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageMemoryUtilization"
    }

    target_value = 75.0
    scale_in_cooldown = 300
    scale_out_cooldown = 60
  }
}