본문으로 건너뛰기
글 목록으로 돌아가기
이 글 목차

ECS Autoscaling 패턴

Race condition 방지를 위한 마이그레이션 태스크 분리와 ECS 서비스 autoscaling 구현 가이드.

마이그레이션 태스크 분리

문제

ECS가 scale out할 때 여러 컨테이너가 동시에 시작돼요. 각각이 데이터베이스 마이그레이션을 실행하면:

Container 1: Running migration...
Container 2: Running migration... (충돌!)
Container 3: Running migration... (충돌!)

해결 방법

마이그레이션을 별도의 task definition으로 분리해서 서비스 scaling 전에 한 번만 실행해요.

# 마이그레이션 태스크 - 한 번만 실행
resource "aws_ecs_task_definition" "migration" {
  family = "${var.project}-migration"
  container_definitions = jsonencode([{
    name    = "migration"
    image   = var.image
    command = ["npm", "run", "migration:run"]
  }])
}

# 서비스 태스크 - 여러 인스턴스로 실행
resource "aws_ecs_task_definition" "service" {
  family = "${var.project}-service"
  container_definitions = jsonencode([{
    name    = "api"
    image   = var.image
    command = ["npm", "run", "start:prod"]
  }])
}

배포 순서

1. 마이그레이션 태스크 실행(단일 인스턴스, 완료 대기)
2. 서비스 task definition 업데이트
3. Autoscaling이 인스턴스 수를 관리하게 맡기기

Target Tracking 정책

권장 설정

resource "aws_appautoscaling_target" "ecs" {
  service_namespace  = "ecs"
  resource_id        = "service/${var.cluster}/${var.service}"
  scalable_dimension = "ecs:service:DesiredCount"
  min_capacity       = 1
  max_capacity       = 4
}

resource "aws_appautoscaling_policy" "cpu" {
  name               = "${var.project}-cpu-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace

  target_tracking_scaling_policy_configuration {
    target_value       = 70  # CPU 목표 퍼센트
    scale_in_cooldown  = 300 # 5분(보수적)
    scale_out_cooldown = 60  # 1분(빠른 대응)

    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
  }
}

resource "aws_appautoscaling_policy" "memory" {
  name               = "${var.project}-memory-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace

  target_tracking_scaling_policy_configuration {
    target_value       = 80  # 메모리 목표 퍼센트
    scale_in_cooldown  = 300
    scale_out_cooldown = 60

    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageMemoryUtilization"
    }
  }
}

이 값들을 선택한 이유

파라미터이유
CPU 목표70%스파이크를 위한 여유 확보
메모리 목표80%메모리는 CPU보다 스파이크가 적음
Scale-out 쿨다운60초부하에 빠르게 대응
Scale-in 쿨다운300초플래핑 방지
최소 용량1비용 최적화
최대 용량4커넥션 제한 내 유지

Connection Pool 계산

최대 용량을 정할 때 중요한 계산이에요:

Max Connections = Max Tasks × Connections per Task
Connection ceiling = DB parameter group이 실제로 허용하는 값

예시(제 환경 기준):
- 4 tasks × 20 connections = 80 connections
- Connection ceiling = ~90-100
- 여유분 = 10-20 connections ✅

여기서 하나 정정할 게 있어요. 저는 이 90-100이라는 숫자를 db.t4g.medium의 한계인 것처럼 적어뒀는데, 사실이 아니에요. RDS는 max_connections 기본값을 인스턴스 메모리에서 계산해요. MySQL은 {DBInstanceClassMemory/12582880}, PostgreSQL은 LEAST({DBInstanceClassMemory/9531392}, 5000)이라서 4 GiB 클래스면 100이 아니라 수백 대가 나와요. 즉 90-100은 인스턴스 클래스의 특성이 아니라 제 환경에 설정돼 있던 ceiling이었어요.

그러니 제 숫자를 그대로 가져다 쓰지 말고 본인 parameter group을 확인하세요(MySQL이면 SHOW GLOBAL VARIABLES LIKE 'max_connections'). 숫자는 환경마다 달라도, 데이터베이스에서 거꾸로 계산해서 최대 용량을 정한다는 방법 자체는 그대로 유효해요.

WebSocket 고려사항

Graceful 처리

  • 프론트엔드에서 scale 이벤트 중 재연결을 처리해야 해요
  • Session affinity 불필요(stateless 설계). 이미 연결된 WebSocket은 upgrade를 받아준 target에 그대로 붙어 있고, 그 컨테이너가 사라지면 ALB가 재연결을 아무 healthy target으로 보내주면 돼요
  • Scale-in 시 connection draining 필요. Target group의 deregistration delay는 기본값이 300초이고, ELB는 그 시간만큼 기다린 뒤에 deregistration을 끝내요. 가장 오래 걸리는 요청 기준으로 조정하세요

WAF Allowlist

WebSocket 경로를 WAF에 추가하는 걸 잊지 마세요:

# Socket.IO 경로 allowlist
byte_match_statement {
  search_string         = "/socket.io"
  positional_constraint = "STARTS_WITH"
  # ...
}

테스트 체크리스트

프로덕션 배포 전:

  • Scale-out(1 → 2+ tasks) CPU > 70%일 때
  • 최대치까지 scale-out(1 → 4 tasks)
  • 부하 감소 후 scale-in(4 → 1 tasks)
  • Connection pool이 제한 내에 유지되는지
  • Scaling 이벤트 중 5XX 에러 없는지
  • WebSocket 재연결이 작동하는지
  • 마이그레이션이 여러 번 실행되지 않는지

핵심 교훈

  1. 마이그레이션 분리 - 서비스 컨테이너에서 마이그레이션을 실행하지 마세요
  2. 보수적인 scale-in - 5분 쿨다운으로 플래핑을 방지해요
  3. 커넥션 제한 우선 - DB 제한에서 최대 용량을 산출하세요
  4. 트래픽이 적을 때 테스트 - rollback 명령어를 준비해두세요
  5. CloudWatch 모니터링 - 최대 용량 시나리오에 알람을 설정하세요

댓글

글 목록으로 돌아가기
enko