ECS를 시간 스케줄로 자동 스케일링하기 (EventBridge 2가지 방법)

AWS ECS 서비스를 CPU·메모리가 아니라 정해진 시간에 스케일 인/아웃하는 방법을 콘솔 기준으로 정리했다. EventBridge Rule + SSM Automation 문서 방식과, SSM 없이 끝내는 EventBridge Scheduler universal target 방식을 둘 다 단계별로 다룬다.

aborts.403 ·
ECS를 시간 스케줄로 자동 스케일링하기 (EventBridge 2가지 방법)

트래픽이 시간대별로 뻔하게 움직이는 서비스가 있다. 사내 업무용 시스템은 출근 시간에 몰리고 새벽엔 거의 놀고, 배치는 정해진 시간대에만 부하가 튄다. 이런 서비스를 CPU 점유율(target tracking)에만 맡겨두면 스케일 아웃이 늘 한 박자 늦게 따라붙어 아침 첫 트래픽에서 버벅인다. 반대로 새벽 내내 쓸 일 없는 태스크가 떠 있어 비용만 새기도 한다.

이럴 땐 CPU 말고 그냥 시간으로 태스크 수를 늘렸다 줄였다 예약하는 편이 낫다. ECS에서 이걸 거는 트리거가 EventBridge인데, 막상 해보면 길이 두 갈래로 갈린다. 예전부터 쓰던 EventBridge Rule에 SSM Automation 문서를 물리는 방식이 하나, 비교적 나중에 나온 EventBridge Scheduler로 끝내는 방식이 또 하나다. 둘 다 콘솔에서 클릭으로 만들 수 있어서, 여기선 양쪽을 다 정리했다.

🗺️ 왜 길이 두 갈래인가

방식이 둘로 나뉘는 이유부터 알고 가면 덜 헷갈린다.

EventBridge Rule(이벤트 규칙)은 부를 수 있는 대상이 정해져 있다. Lambda, Step Functions, SSM Automation, SNS, SQS, ECS RunTask 정도다. 여기엔 ecs:UpdateService 같은 임의의 AWS API를 곧장 때리는 길이 없다. 그래서 중간에 다리를 하나 놔야 했고, 그 다리로 가장 많이 쓰던 게 SSM Automation 문서였다. 문서 안에서 ecs:UpdateService를 호출해 태스크 수를 바꾸는 식이다. 내가 처음 이걸 붙일 때도 이 방법이었다.

그런데 2022년 말에 나온 EventBridge Scheduler는 사정이 다르다. universal target이라는 게 있어서 AWS API를 6,000개 넘게 직접 부른다. ecs:UpdateService도 그중 하나라, SSM 문서나 Lambda 없이 스케줄이 바로 태스크 수를 바꿔버린다. 다리가 통째로 사라지는 셈이다.

구분중간 다리(SSM·Lambda)특징
EventBridge Rule필요기존 자산이 있거나 손보는 단계가 많을 때
EventBridge Scheduler불필요새로 만들 때 제일 간단, 타임존도 기본 지원

아래 예시는 prod-clusterweb-api 서비스를 평일 오전 9시엔 8대, 밤 10시엔 2대로 맞추는 상황으로 잡았다.

📋 시작 전에 — 서비스부터 확인

손대기 전에 조절할 서비스가 지금 몇 대로 떠 있는지부터 본다. ECS 콘솔에서 클러스터를 거쳐 서비스로 들어가면 Desired/Running 태스크 수가 나온다. 이때 클러스터 이름과 서비스 이름을 정확히 적어두자. 뒤에서 그대로 갖다 쓴다.

만약 이 서비스에 CPU 기반 오토스케일링(Application Auto Scaling)이 이미 걸려 있다면, 시작하기 전에 맨 아래 주의점부터 읽고 오는 게 좋다. 시간 스케줄과 CPU 정책이 같은 태스크 수를 놓고 서로 밀고 당길 수 있어서다.

🛠️ 방법 A — EventBridge Rule + SSM 문서

이미 EventBridge Rule이나 SSM을 쓰고 있는 환경이면 이쪽이 손에 익는다. 문서를 한 번 만들어두면 다른 서비스에도 그대로 돌려쓸 수 있다는 것도 장점이다.

SSM Automation 문서 만들기

Systems Manager 콘솔에서 Documents → Create automation으로 들어가 문서를 하나 만든다. 내가 쓰는 문서는 스텝이 둘이다. 먼저 ecs:UpdateService로 태스크 수를 바꾸고, 이어서 application-autoscaling:RegisterScalableTarget으로 오토스케일링 Min/Max 경계까지 같이 옮긴다. 두 번째 스텝이 왜 필요한지는 잠시 뒤에 설명한다. 본문(YAML)에서 중요한 부분만 추리면 이렇다.

schemaVersion: '0.3'
description: Update ECS Service Desired Count and Auto Scaling Min/Max Capacity
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  AutomationAssumeRole: { type: String }
  ClusterName:  { type: String }
  ServiceName:  { type: String }
  DesiredCount: { type: Integer }
  MinCapacity:  { type: Integer }
  MaxCapacity:  { type: Integer }
mainSteps:
  # ① 서비스 desired count 변경
  - name: UpdateDesiredCount
    action: aws:executeAwsApi
    nextStep: UpdateScalingCapacity
    inputs:
      Service: ecs
      Api: UpdateService
      cluster: '{{ ClusterName }}'
      service: '{{ ServiceName }}'
      desiredCount: '{{ DesiredCount }}'
  # ② 오토스케일링 Min/Max 경계도 같이 조정
  - name: UpdateScalingCapacity
    action: aws:executeAwsApi
    isEnd: true
    inputs:
      Service: application-autoscaling
      Api: RegisterScalableTarget
      ServiceNamespace: ecs
      ResourceId: 'service/{{ ClusterName }}/{{ ServiceName }}'
      ScalableDimension: ecs:service:DesiredCount
      MinCapacity: '{{ MinCapacity }}'
      MaxCapacity: '{{ MaxCapacity }}'

값을 전부 파라미터로 빼둔 게 요령이다. 그래야 같은 문서 하나를 두고 아침(8대)과 밤(2대)에서 값만 갈아끼워 돌려쓸 수 있다. executeAwsApicluster·service·desiredCount는 ECS API가 받는 소문자 이름 그대로 쓴다는 점만 주의하면 된다.

SSM Automation 문서 ECS_UPDATE_SERVICE_RUNBOOK — UpdateService와 RegisterScalableTarget 두 스텝으로 구성된 YAML 본문

EventBridge Rule로 스케줄 걸기

이제 EventBridge 콘솔에서 Rules → Create rule로 규칙을 만든다. 일정 정의 단계에서 cron 식으로 시간을 잡는다.

EventBridge 규칙 생성의 일정 정의 단계 — 분·시간·일·월·요일·연도 6개 칸으로 된 cron 식 입력 화면

대상 선택에서는 대상 유형을 AWS 서비스로 두고 Systems Manager 자동화를 고른다. 문서는 방금 만든 ECS_UPDATE_SERVICE_RUNBOOK을 지정하고, 자동화 파라미터는 상수로 채운다. 아침에 늘리는 규칙이면 DesiredCountMinCapacity를 8로, MaxCapacity는 평소 상한으로 넣는 식이다. 마지막에 문서를 실행할 IAM 역할을 지정한다.

EventBridge 규칙 대상 선택 — Systems Manager 자동화에서 ECS_UPDATE_SERVICE_RUNBOOK 문서를 상수 파라미터로 지정한 화면

밤에 줄이는 규칙은 같은 순서로 하나 더 만든다. cron만 밤 10시로, DesiredCountMinCapacity만 2로 바꾸면 된다.

여기서 한 가지 조심할 게 있다. EventBridge Rule의 cron은 UTC로 돈다. 타임존을 따로 지정하는 칸이 없다. 그래서 한국 시간 오전 9시를 원하면 cron엔 0시(0 0)로 적어야 한다. 이걸 모르고 그대로 9시로 적으면 9시간이 밀린다. 뒤에 나올 Scheduler와 가장 크게 갈리는 지점이다.

⚡ 방법 B — EventBridge Scheduler (SSM 없이)

새로 세팅하는 거라면 굳이 SSM 문서까지 만들 필요 없이 이쪽이 훨씬 간단하다. 중간 다리가 없고, 타임존도 콘솔에서 바로 고른다.

EventBridge 콘솔 왼쪽 메뉴에서 Scheduler → Schedules → Create schedule로 들어간다. 일정은 반복 일정으로 두고 cron을 적은 뒤, 시간대를 Asia/Seoul로 맞춘다. 이 칸 하나 때문에 방법 A의 UTC 환산 수고가 사라진다. 입력하면 아래에 다음 트리거 날짜가 미리 찍혀 나오니, 의도한 시각이 맞는지 바로 확인할 수 있다.

EventBridge Scheduler 일정 생성 — 반복 일정, 시간대 Asia/Seoul, Cron 기반 일정으로 설정하고 다음 트리거 날짜가 표시된 화면

Flexible time window(유연한 기간)는 선택 안 함으로 둔다. 켜두면 지정한 시간 안에서 알아서 분산 실행돼 정각이 보장되지 않는다.

타깃을 고를 때가 핵심이다. 템플릿 타깃 말고 “All APIs”(universal target)를 골라 ECS를 검색하고 UpdateService를 선택한다. 콘솔에 보이는 ECS 템플릿 타깃은 RunTask용이라 태스크 대수를 조절하는 데는 맞지 않는다. 여기서 한 번 헷갈리기 쉽다. 그다음 Input 칸에 호출 파라미터를 JSON으로 넣는다.

{
  "Cluster": "prod-cluster",
  "Service": "web-api",
  "DesiredCount": 8
}

여기선 키 이름을 대문자로 시작해야 한다. SSM 문서에서 쓰던 소문자 cluster·service와 달리, universal target의 Input은 Cluster·Service·DesiredCount처럼 PascalCase다(콘솔에도 “파라미터 이름은 PascalCase여야 합니다”라고 안내가 뜬다). 소문자로 적으면 조용히 안 먹는다. 나도 여기서 한참 봤다.

EventBridge Scheduler 대상 선택 — 모든 API에서 Amazon ECS UpdateService를 고르고 Cluster·Service·DesiredCount를 담은 Input JSON을 입력한 화면

마지막 권한 단계에서 실행 역할을 새로 만들거나 이미 있는 걸 고른다. 어느 쪽이든 이 역할에 ecs:UpdateService 권한이 들어 있어야 한다.

EventBridge Scheduler 권한 단계 — 기존 실행 역할(ecsEventsRole)을 선택한 화면

밤에 줄이는 스케줄도 똑같이 하나 더 만든다. cron을 밤 10시로, DesiredCount를 2로 바꾸면 끝이다.

🌍 cron이랑 타임존에서 한 번씩 막힌다

방식이 뭐든 cron에서 걸리는 지점은 똑같다.

EventBridge Scheduler의 cron은 필드가 6개다(cron(분 시 일 월 요일 연도)). 평일 오전 9시는 이렇게 쓴다.

cron(0 9 ? * MON-FRI *)

여기서 두 가지를 자주 틀린다. 하나는 일(day-of-month)과 요일(day-of-week) 자리를 동시에 *로 못 둔다는 점이다. 둘 중 안 쓰는 쪽은 반드시 ?로 비워야 한다. 위 예시는 요일을 썼으니 일 자리가 ?다. 다른 하나는 타임존인데, 안 적으면 UTC로 돈다. Scheduler는 콘솔에서 Asia/Seoul을 고르면 그만이지만, 방법 A의 EventBridge Rule은 그 칸이 없어서 cron을 직접 UTC로 환산해 넣어야 한다(오전 9시면 0 0).

✅ 진짜 도는지 확인하기

등록만 됐는지가 아니라, 시간이 지난 뒤에 태스크 수가 실제로 움직였는지를 봐야 한다. ECS 서비스 상세에서 Desired/Running이 바뀌었는지 확인하고, 방법 A면 SSM의 Automation 실행 이력을, 방법 B면 Scheduler의 실행 메트릭을 같이 들여다본다.

값이 그대로면 십중팔구 cron의 ? 규칙이나 타임존, 아니면 실행 역할에 ecs:UpdateService 권한이 빠진 경우다.

⚠️ 미리 알아두면 좋은 것들

앞에서 미룬 얘기를 여기서 한다. UpdateService로 태스크 수만 바꾸고 끝내면, CPU 기반 target tracking이 같이 걸린 서비스에선 오토스케일링이 다음 평가 때 그 값을 도로 끌어올린다. 스케줄로 2대까지 내려놨는데 정책이 다시 8대로 올려버리는 식이다. 그래서 방법 A의 문서가 두 번째 스텝에서 RegisterScalableTarget으로 Min/Max 경계 자체를 같이 옮긴다. 경계를 내려놔야 desired도 그 안에 머문다. 방법 B(Scheduler)는 universal target이 한 번에 API 하나만 부르니, 경계까지 손대려면 application-autoscaling:RegisterScalableTarget을 호출하는 스케줄을 하나 더 거는 식으로 보완해야 한다.

권한은 호출하는 API를 따라간다. 방법 A의 문서는 ecs:UpdateServiceapplication-autoscaling:RegisterScalableTarget 둘 다 필요하고(문서의 AutomationAssumeRole에), 거기에 EventBridge가 그 문서를 실행할 권한이 더 붙는다. 방법 B는 Scheduler 실행 역할에 같은 API 권한이 있으면 된다.

그리고 스케일 아웃은 버튼 누르듯 즉시 끝나지 않는다. 태스크가 뜨고 헬스체크를 통과하고 로드밸런서에 붙기까지 몇 분은 걸린다. 그러니 트래픽이 몰리는 시각보다 조금 앞당겨 잡아두는 게 안전하다. 9시에 몰린다면 8시 50분쯤 늘려두는 식이다.

어느 쪽을 고를지는 상황 나름이다. 이미 EventBridge Rule이나 SSM을 굴리고 있고 문서를 여기저기 돌려쓰고 싶으면 방법 A, 아무것도 없는 데서 새로 깔끔하게 시작할 거면 방법 B가 편하다.

관련 글

Claude Opus 4.8 vs Fable 5 차이 — 성능·지능·가격으로 보는 모델 선택 가이드
Go(Golang) 프로젝트 폴더 구조 컨벤션 — cmd · internal · pkg 제대로 쓰는 법 (실전 예시 포함)