Code Snip: This sites ECS CLuster

Sunday, Feb 8, 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

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

      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 = aws_ecs_capacity_provider.asg_capacity.name
    weight = 100
    base = 1
  }

  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
  }
}