AWS 타겟 그룹 Unhealthy 알람 — EventBridge · Lambda

클라우드·인프라5분조회 –

ALB 뒤에 붙은 대상이 Unhealthy 로 빠지면 ALB 는 그쪽으로 트래픽을 안 보냅니다. 남은 대상이 있으면 서비스는 그대로 돌아가니 한 대가 빠져도 아무도 모르고 지나갈 수 있고, 그러다 나머지까지 빠지면 그때 장애가 됩니다.

타겟 그룹(Target Group, 이하 TG)이 5분 넘게 Unhealthy 면 슬랙으로 알려 달라는 요구가 들어왔습니다. 한 리전의 TG 전부가 대상이고 쿠버네티스가 자동으로 만드는 것만 뺐습니다. TG 는 서비스가 하나 늘 때마다 같이 생기니 감시할 목록이 고정되어 있지 않다는 게 이 요구의 까다로운 부분입니다.

먼저 CloudWatch Alarm 을 붙여 봤고, 이 요구에는 안 맞았습니다.

CloudWatch Alarm — TG 하나당 알람 하나

TG 가 수십 개면 알람도 그만큼 생기고, 알람 개수만큼 관리 대상이 늘어나니 임계값이나 알림 채널을 한 번 바꾸려면 전부 손봐야 하고, 등록하는 순간 정상인 TG 들이 다 OK 상태로 잡히면서 정상 알림이 개수만큼 한꺼번에 나갑니다. 무엇보다 신규 TG 가 추가될 때마다 알람을 만들어 주는 스크립트를 수동으로 다시 돌려야 하고, 안 돌리면 그 TG 는 감시망 밖에 남습니다.

EventBridge 2분 폴링 + Lambda 하나

Lambda 하나가 전체 TG 를 훑는 구조라 매 실행마다 목록을 새로 조회하니 신규 TG 가 자동으로 들어옵니다.

EventBridge Scheduler (*/2 * * * ? *)
    ↓ 2분마다 트리거
Lambda (Python 3.12)
    ↓
① SSM 스냅샷 조회 (이전 상태 전체 로드)
② ELBv2 API → 전체 TG 조회 (k8s prefix 제외)
③ 각 TG의 describe_target_health
④ EC2 describe_instances 일괄 조회 (Hostname, IP)
⑤ Unhealthy 타겟 처리
⑥ 복구 감지 → chat.update로 [해결] 갱신 후 SSM 삭제

2분 폴링이라 Unhealthy 가 시작되고 최초 감지까지 최대 2분이 늦고, 거기서 5분을 채우는 데 또 한 사이클이 걸려서, 실제 시작 시점 기준으로는 8분 가까이 됩니다.

상태 저장 — SSM Parameter Store

“5분 이상 지속”을 판정하려면 언제부터 Unhealthy 였는지를 기억해야 하지만 Lambda 는 실행 간 상태가 없으니 밖에 둬야 합니다.

키는 TG 이름과 타겟을 붙여 만들고 생성·수정·삭제를 전부 Lambda 가 하기 때문에 사전에 만들어 둘 게 없습니다.

/tg-alarm/{TG이름}/{인스턴스ID}_{포트}

예시:
/tg-alarm/my-app-test-tg/i-0123456789abcdef0_80

SSM 허용 문자 규칙에 맞춰 특수문자는 - 로 치환합니다.

저장하는 값은 이렇습니다.

필드 용도
first_unhealthy_at 최초 감지 시각(UTC). 5분 경과 판단 기준
alerted Slack 발송 여부. 중복 발송 방지
slack_ts Slack 메시지 타임스탬프. chat.update 에 사용
tg_name / lb_names TG 이름, 연결된 LB 이름
hostname / ip / instance_id / port 알림 본문에 넣을 타겟 정보
reason AWS health check 실패 원인 코드 (Target.Timeout 등)

상태 변화는 세 단계입니다.

T+0분   최초 감지  → SSM 생성     (alerted=False, slack_ts=null)
T+6분   5분 경과   → SSM 업데이트 (alerted=True,  slack_ts 저장)
T+10분  복구 감지  → SSM 삭제

현재 상태는 콘솔에서 바로 보입니다.

aws ssm get-parameters-by-path \
  --path /tg-alarm \
  --recursive \
  --region ap-northeast-2

지금 감시 중인 TG 가 몇 개고 각각 언제부터 Unhealthy 인지가 한 번에 나옵니다.

5분 이내 복구 — 알림 생략

alerted 가 판단 기준입니다.

복구를 감지했을 때 alerted=True 면 이미 알람을 보낸 건이라 chat.update 로 원래 메시지를 [해결] 로 바꾸고 SSM 키를 지웁니다. 새 메시지를 쌓지 않아서 채널이 깨끗하게 유지됩니다.

alerted=False 면 5분을 못 채우고 복구된 건이라 Slack 없이 SSM 키만 지우니, 일시적인 장애가 채널에 남지 않습니다.

복구 후 재발생하면 SSM 키가 없는 상태라 최초 감지와 똑같이 다시 시작합니다.

알람이 한 번도 안 나온 이유 — 스냅샷 읽는 시점

계속 Unhealthy 인 TG 인데도 알람이 한 번도 안 오는 건이 있었습니다. 처음에는 Unhealthy 처리 루프를 돌면서 SSM 을 읽고 썼고, 그 다음에 복구 감지 루프를 돌렸습니다.

# 잘못된 순서 (버그 발생)
unhealthy 처리 루프 실행
    → SSM에 새 키 생성
ssm_get_all() 호출
    → 방금 생성한 키도 포함됨
복구 루프에서 해당 키를 복구로 잘못 처리
    → SSM 삭제 → 재발생 알람 영원히 안 옴

키가 지워지니 다음 사이클에서 first_unhealthy_at 부터 다시 시작하고, 5분이 채워질 일이 없습니다.

고친 건 순서 하나입니다.

# 올바른 순서 (채택)
prev_ssm_states = ssm_get_all()   ← 맨 처음에 스냅샷
unhealthy 처리 루프 → prev_ssm_states 기준으로 판단
복구 루프          → prev_ssm_states 기준으로 비교

증상이 “알람이 안 온다”라서 발송 로직을 들여다보기 쉬운데, 원인은 삭제 쪽에 있었습니다.

EC2 정보 일괄 조회

Unhealthy 타겟의 호스트명과 사설 IP 를 알림에 넣으려면 EC2 조회가 필요합니다. TG 를 돌면서 타겟마다 부르면 호출 수가 타겟 수만큼 늘어납니다.

# 비효율적인 방식
for tg in tgs:
    for target in targets:
        ec2.describe_instances(InstanceIds=[target])  # TG마다 API 호출

# 채택 방식
all_instance_ids = [모든 unhealthy 타겟 ID 수집]
ec2.describe_instances(InstanceIds=all_instance_ids)  # 단 1회 호출

TG 마다 부르면 호출 수가 TG 개수만큼 늘고 Lambda 실행 시간도 그만큼 길어지니, ID 를 다 모아 한 번에 물으면 한 번으로 끝납니다.

동시 실행 차단 — Reserved concurrency 1

2분 주기라 이전 실행이 아직 안 끝났으면 다음 실행이 겹치고, 두 실행이 같은 SSM 키를 보고 각자 Slack 을 쏘면 알림이 두 번 갑니다. Reserved concurrency 를 1로 두면 겹치는 실행이 throttle 되면서 이 경로가 막힙니다.

알람을 늘리는 대신 목록을 매번 조회

CloudWatch Alarm 은 대상 하나당 알람 하나라 대상이 늘면 관리할 것도 같이 늘어납니다. 대상 목록이 계속 바뀌는 환경이면 알람을 미리 만들어 두는 대신 매 실행마다 목록을 새로 조회하는 쪽이 손이 덜 가고, 대신 지속 시간을 판정할 상태를 밖에 둬야 하는데, SSM Parameter Store 정도면 충분했습니다.

상태를 밖에 두면 그걸 언제 읽느냐가 곧 버그 자리가 됩니다. 이번 것도 루프 중간에 다시 읽는 바람에 방금 만든 키를 복구로 오인한 것이라 사이클 시작에서 한 번 스냅샷을 뜨고 그 스냅샷으로만 판단하게 바꿨습니다. 폴링으로 상태를 관리하는 코드를 짤 때는 읽는 시점을 하나로 고정해 두면 이런 종류가 안 생깁니다.

  1. 불러오는 중