배경
주말에 ECS 앱이 컨테이너 이미지를 찾지 못하는 에러(CannotPullContainerError)가 발생하여 서비스가 중단되었다. 문제는 즉시 감지되지 않았고, 업무 영향도 파악 요청이 들어온 후에야 인지되었다.
이를 계기로 ECS Task가 STOPPED 상태로 전환될 때 즉시 알람을 받을 수 있는 모니터링을 구축했다.
CloudWatch vs EventBridge 비교
| 감시 항목 | CloudWatch 지표 | EventBridge 이벤트 |
|---|---|---|
| ECS 서비스 전체 상태 | 가능 | 가능 |
| 개별 태스크 STOPPED 감지 | 불가능 | 가능 |
| STOPPED 사유 확인 (CannotPullContainerError 등) | 불가능 | 가능 |
| 알람 연동 (SNS, 이메일 등) | 가능 (지표 기준) | 가능 (이벤트 기준) |
Task 상태만 확인하면 되는 상황이므로 CloudWatch 지표(RunningTaskCount) 방식을 선택했다.
개별 태스크의 STOPPED 사유까지 추적해야 한다면 EventBridge 이벤트 룰을 사용하는 것이 적합하다.
아키텍처
ECS Service (RunningTaskCount 지표)
↓
CloudWatch Alarm (RunningTaskCount <= 0, 5분 내 5회 연속)
↓
SNS Topic (ecs-task-alerts)
↓
Lambda (SendSlackNotification)
↓
Slack Webhook → 채널 알림
구축 과정
1. Slack Webhook URL 생성
Slack 앱에서 Incoming Webhook을 생성하여 알림을 받을 채널에 연결한다.
Slack 워크스페이스 > 앱 관리 > Incoming Webhooks > 새 Webhook 생성
→ 알림 채널 선택 → Webhook URL 복사
2. Lambda 함수 생성 (Slack 알림 처리)
SNS 메시지를 받아 Slack으로 전달하는 Lambda 함수를 생성한다.
import json
import urllib.request
import os
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
def lambda_handler(event, context):
# SNS 메시지 파싱
sns_message = event['Records'][0]['Sns']
subject = sns_message.get('Subject', 'ECS Alert')
message = json.loads(sns_message['Message'])
alarm_name = message.get('AlarmName', 'Unknown')
new_state = message.get('NewStateValue', 'Unknown')
reason = message.get('NewStateReason', '')
# Slack 메시지 구성
slack_message = {
"text": f":rotating_light: *ECS Task Alert*",
"attachments": [
{
"color": "#ff0000" if new_state == "ALARM" else "#36a64f",
"fields": [
{"title": "Alarm", "value": alarm_name, "short": True},
{"title": "State", "value": new_state, "short": True},
{"title": "Reason", "value": reason[:200], "short": False},
],
}
],
}
req = urllib.request.Request(
SLACK_WEBHOOK_URL,
data=json.dumps(slack_message).encode('utf-8'),
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req)
return {'statusCode': 200}환경 변수:
SLACK_WEBHOOK_URL: Slack Webhook URL
3. SNS 주제 생성
AWS 콘솔 → SNS > 주제 > 새 주제 생성
이름: ecs-task-alerts
유형: 표준(Standard)
4. CloudWatch Alarm 생성
각 ECS 서비스마다 RunningTaskCount 알람을 설정한다.
import boto3
cloudwatch = boto3.client('cloudwatch')
def create_ecs_task_alarm(cluster_name, service_name, alarm_name, sns_topic_arn):
cloudwatch.put_metric_alarm(
AlarmName=alarm_name,
MetricName='RunningTaskCount',
Namespace='AWS/ECS',
Dimensions=[
{'Name': 'ClusterName', 'Value': cluster_name},
{'Name': 'ServiceName', 'Value': service_name},
],
Statistic='Average',
Period=60, # 1분 간격 수집
EvaluationPeriods=5, # 5개 데이터 포인트
Threshold=0,
ComparisonOperator='LessThanOrEqualToThreshold',
AlarmActions=[sns_topic_arn],
TreatMissingData='breaching',
)알람 조건:
- 지표:
RunningTaskCount - 조건:
<= 0 - 기간: 1분 간격, 5분 내 5개 데이터 포인트 연속 충족 시 알람
- 알림: SNS 주제
ecs-task-alerts
5. SNS → Lambda 연결
Lambda > SendSlackNotification 함수 > 트리거 추가
트리거 유형: SNS
SNS 주제: ecs-task-alerts
→ Lambda IAM 역할에 SNS 실행 권한 자동 부여
알람 흐름 정리
1. ECS 서비스의 Task가 모두 중지됨 (RunningTaskCount = 0)
2. CloudWatch가 1분 간격으로 수집, 5분 연속 0 감지
3. CloudWatch Alarm → ALARM 상태 전환
4. SNS Topic (ecs-task-alerts)에 알림 발행
5. Lambda (SendSlackNotification) 트리거
6. Slack 채널에 알람 메시지 전송
설계 포인트
5분 연속 조건으로 오탐 방지 배포 중 일시적으로 Task가 0이 되는 상황(롤링 업데이트 등)에서 불필요한 알람이 발생하지 않도록 5회 연속 조건을 설정했다.
TreatMissingData = breaching 데이터 포인트가 누락되는 경우에도 알람이 발생하도록 설정한다. 서비스 자체가 삭제되거나 메트릭 수집이 불가능한 상황도 감지한다.
서비스별 개별 알람 각 ECS 서비스마다 별도 알람을 생성하여 어떤 서비스에서 문제가 발생했는지 즉시 식별할 수 있다.
정리
- ECS RunningTaskCount 지표 기반 Task 중단 감지
- CloudWatch Alarm → SNS → Lambda → Slack 알림 파이프라인 구축
- 5분 연속 조건으로 배포 중 오탐 방지
- 서비스별 개별 알람으로 장애 서비스 즉시 식별
- 주말/야간 장애 인지 지연 문제 해결
배운 점
주말 장애를 사람이 먼저 알려주는 상황만큼 곤란한 건 없다. 이 경우 문제는 장애 자체가 아니라 인지 경로가 없다는 것이었다.
지표와 이벤트의 차이를 확실히 알게 된 작업이기도 하다. RunningTaskCount는 몇 개가 떠 있는지만 알려주고, 왜 죽었는지는 EventBridge 이벤트에만 있다. 감지는 지표로 하고 원인은 이벤트로 본다는 조합이 이때 정리됐다.
배포 중에도 Task 수는 잠깐 줄어든다. 그래서 5분 연속 조건을 붙였는데, 이건 EDI Agent 중앙 모니터링의 연속 감지와 같은 고민이다. 정상 작업과 장애를 구분하지 못하는 알림은 결국 꺼진다.
이어 읽기
- ECS Fargate 컨테이너 Health Check 기반 자동 재시작 구현: Task가 살아 있어도 앱이 죽는 경우
- CloudWatch 알람 기반 EC2/ECS 통합 모니터링 구축: 이 알람이 속한 전체 알람 설계