개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← YouTube Shorts 자동화

스케줄러 전환 — EventBridge와 CloudWatch 모니터링

2026년 7월 17일
EventBridgeLambdaCloudWatchscheduler

지금까지 스케줄러는 API 서버(NestJS) 안에서 @Cron('* * * * *')로 1분마다 돌고 있었다. Phase 5에서 이걸 통째로 갈아엎었다.

API 내장 @Cron → 독립 scheduler-worker Lambda

API 서버가 죽거나 재배포되면 스케줄러도 같이 멈춘다. Auto Scaling도 안 걸린다. 이건 별도 Lambda로 빼야 했다.

// 이전: apps/api/src/scheduler/scheduler.service.ts
@Injectable()
export class SchedulerService {
  @Cron('* * * * *')
  async tick() {
    const channels = await this.channelsRepo.getEnabledSchedules();
    for (const ch of channels) {
      if (!ch.uploadSchedule || !shouldRunNow(ch.uploadSchedule)) continue;
      // ... Job 생성 ...
    }
  }
}

이 로직을 그대로 뽑아서 apps/workers/scheduler라는 독립 Lambda로 옮겼다. 트리거는 EventBridge rate(1 minute).

# apps/workers/scheduler/serverless.yml
functions:
  handler:
    handler: src/handler.handler
    timeout: 60
    events:
      - schedule:
          rate: rate(1 minute)
          enabled: true

핸들러 안에서는 Prisma로 직접 채널을 조회하고, cron 표현식이 지금 시각과 맞는 채널만 골라서 SQS로 Job 생성 메시지를 보낸다. API 서버 쪽 SchedulerService는 빈 껍데기로 남기고 SchedulerModule도 app.module.ts에서 뺐다.

dlq-notifier — 죽은 메시지를 Slack으로

SQS 메시지가 3번 재시도 후 실패하면 DLQ(Dead Letter Queue)로 빠지는데, 지금까지는 이걸 CloudWatch 콘솔 들어가서 눈으로 확인해야 했다. 그래서 DLQ 5개(script/tts/subtitle/render/upload)를 트리거로 문 Lambda 하나를 새로 만들었다.

// apps/workers/dlq-notifier/src/handler.ts
export const handler: SQSHandler = async (event) => {
  for (const record of event.Records) {
    const queueName = record.eventSourceARN.split(':').pop() ?? record.eventSourceARN;
    const label = QUEUE_LABELS[queueName] ?? queueName;
    const receiveCount = record.attributes.ApproximateReceiveCount;

    let jobId = '알 수 없음';
    let channelId = '알 수 없음';
    try {
      const parsed = JSON.parse(record.body) as Record<string, unknown>;
      jobId = String(parsed['jobId'] ?? '알 수 없음');
      channelId = String(parsed['channelId'] ?? '알 수 없음');
    } catch {
      // 파싱 실패 시 원본 body 유지
    }

    await postWebhook(WEBHOOK_URL, `🚨 DLQ 알림 — ${label}\n큐: ${queueName}\nJob ID: ${jobId}...`);
  }
};

메시지 body가 항상 정상 JSON이라는 보장이 없어서 파싱 실패 시에도 원본 body를 그대로 보여주도록 방어했다.

CloudWatch 알람 — Lambda 에러율과 DLQ 깊이

여기까지 하고 나니 "그래서 애초에 왜 DLQ까지 갔는지"를 알람으로도 받고 싶어졌다. Terraform으로 두 종류를 추가했다.

# Lambda 에러율 > 5% (5분 윈도우)
resource "aws_cloudwatch_metric_alarm" "lambda_error_rate" {
  for_each = local.lambda_workers
  metric_query {
    id         = "error_rate"
    expression = "IF(invocations > 0, errors / invocations * 100, 0)"
    return_data = true
  }
  threshold           = 5
  comparison_operator = "GreaterThanThreshold"
}

# DLQ 메시지 1개 이상 쌓이면 즉시 알림
resource "aws_cloudwatch_metric_alarm" "dlq_depth" {
  for_each   = toset(["prod-script-queue-dlq", "prod-tts-queue-dlq", ...])
  metric_name = "ApproximateNumberOfMessagesVisible"
  threshold   = 0
}

두 알람 모두 SNS 토픽 하나로 묶고 이메일 구독을 연결했다. 이 시점부터는 워커가 죽으면 콘솔을 안 봐도 알 수 있게 됐다.

정리 — 안 쓰는 ECS/Fargate 잔재 제거

스케줄러까지 Lambda로 옮기고 나니 ECS/Fargate 관련 Terraform 모듈(ecs-cluster)이랑 API의 scheduler 스텁 코드가 완전히 죽은 코드가 됐다. 이번 기회에 같이 걷어냈다.

← 이전 글Lambda 안정화 — 배포와 자막 싱크 보정
다음 글 →IAM 정비 — 배포 권한과 Phase 재정렬