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

© 2026 newgirok

← YouTube Shorts 자동화

알림 통합 — DLQ·CloudWatch Slack 리팩터

2026년 8월 2일
cloudwatch-notifierdlq-notifierSlackBlock Kit

Lambda 에러율 알람이 이메일로 왔다. 안 읽는다. Slack으로 옮기고, DLQ 깊이 알람은 아예 없앴다. dlq-notifier 하나가 이미 실패 건을 Slack으로 물어다주고 있어서 굳이 별도 알람이 필요 없었다.

cloudwatch-notifier — 새 Lambda

CloudWatch 알람 상태 변화를 SNS로 받아 Slack에 쏘는 워커를 새로 만들었다.

const _handler: SNSHandler = async (event) => {
  for (const record of event.Records) {
    const alarm = JSON.parse(record.Sns.Message) as CloudWatchAlarm;
    const isAlarm = alarm.NewStateValue === 'ALARM';

    const functionName =
      alarm.Trigger.Dimensions.find((d) => d.name === 'FunctionName')?.value ?? '알 수 없음';
    const workerName = functionName.replace('shorts-', '').replace('-prod-handler', '');
    ...
    await postSlack(SLACK_WEBHOOK_URL, text);
  }
};

처음엔 그냥 텍스트 한 줄이었다. 이게 이 뒤로 세 라운드에 걸쳐 계속 바뀐다.

1라운드 — Block Kit 카드로

텍스트 알림은 정보가 다 붙어서 나와서 읽기 불편했다. Block Kit으로 색상바 + 필드 + 로그 명령어까지 붙인 카드 형식으로 바꿨다.

const payload = {
  attachments: [
    {
      color: isAlarm ? '#e01e5a' : '#2eb886',
      blocks: [
        { type: 'section', text: { type: 'mrkdwn', text: `${isAlarm ? '🔴' : '🟢'} *[PRD] ${functionName} • ${workerName}*\n${alarm.NewStateValue} | ${alarm.AlarmName}` } },
        { type: 'divider' },
        { type: 'section', fields: [
          { type: 'mrkdwn', text: `🖥️ *Server*\n\`${functionName}\`` },
          { type: 'mrkdwn', text: `📦 *Container*\n${workerName}` },
          ...
        ] },
        { type: 'context', elements: [{ type: 'mrkdwn', text: `\`${logCommand}\`` }] },
      ],
    },
  ],
};

aws logs tail 명령어까지 카드 하단에 박아뒀다. 알림 보고 바로 복붙해서 로그 확인하려고.

버그 — Dimensions undefined

이 와중에 Dimensions.find()가 터졌다. SNS 메시지에 Dimensions 필드가 아예 없는 알람 타입이 있었다.

-      alarm.Trigger.Dimensions.find((d) => d.name === 'FunctionName')?.value ?? '알 수 없음';
+      alarm.Trigger.Dimensions?.find((d) => d.name === 'FunctionName')?.value ?? '알 수 없음';

옵셔널 체이닝 하나 빠뜨린 거였다. 같은 커밋에서 워커들의 prisma.job.update도 updateMany로 바꿨다. update는 대상 row가 없으면 P2025 에러를 던지는데, updateMany는 조용히 0건 처리하고 넘어간다. 존재하지 않는 jobId로 상태 업데이트를 시도하는 경우가 실제로 있었다.

2라운드 — 필드 가로 배치가 안 읽혀서 세로로

Block Kit의 fields는 2열 grid로 렌더링된다. 필드 이름이 길면 줄바꿈이 이상하게 꺾인다. section.text 하나로 몰아서 세로 배치로 바꿨다.

const infoText = [
  `⚙️  *Function*    \`${functionName}\``,
  `🏷️  *Worker*    ${workerName}`,
  `⏰  *Time*    ${time}`,
  `📊  *Metric*    ${metricName} > ${alarm.Trigger.Threshold}%`,
].join('\n');

같이 잡은 것: Dimensions가 name/value(소문자)로 올 때도 있고 Name/Value(대문자)로 올 때도 있어서, 둘 다 대응하도록 타입을 넓혔다.

const fnDim = alarm.Trigger.Dimensions?.find((d) => (d.name ?? d.Name) === 'FunctionName');
const functionNameFromDimension = fnDim?.value ?? fnDim?.Value;
// Dimensions 없을 때 AlarmName(예: prod-script-error-rate)에서 역산
const workerNameFromAlarm = alarm.AlarmName.replace(/^prod-/, '').replace(/-error-rate$/, '');
const functionName = functionNameFromDimension ?? `shorts-${workerNameFromAlarm}-prod-handler`;

Dimensions가 아예 없으면 알람 이름에서 워커 이름을 역산하는 fallback까지 넣었다.

3라운드 — 제목이 헷갈려서

알람 알림이랑 DLQ 알림 제목이 둘 다 [PRD] {함수명} • {워커명} 형식이라 Slack 채널에서 구분이 안 됐다. DLQ 쪽은 [DLQ] 태그를 붙여서 명확히 갈랐다.

-                text: `🔴 *[PRD] ${functionName} • ${label}*\nDLQ | \`${queueName}\``,
+                text: `🔴 *[DLQ] ${label} — Job 처리 실패*\n${functionName} | ${queueName}`,

배포 알림 제목에서는 반대로 repo명을 뺐다. Slack bot 이름 자체가 이미 repo를 나타내고 있어서 [PRD] {REPO} • Deploy API 처럼 두 번 반복 표기되고 있었다.

-            --arg title   "${ICON} *[PRD] ${REPO} • Deploy API*" \
+            --arg title   "${ICON} *[PRD] Deploy API*" \

한 번에 완성형으로 만들 수 있는 게 아니었다. 알림을 실제로 받아보면서 읽기 불편한 부분을 하나씩 걷어냈다.

← 이전 글예산·에러 감시 — Sentry 연동과 Budget Alert
다음 글 →알람 정비 — SNS 파싱과 KST 변환 수정