FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 운영 인계와 장애 대응 훈련 · 실습
새벽 세 시, 인계 문서의 명령이 틀려 있었다
목표
인계받을 운영팀을 위해 지표 → 경보 조건 → 런북 항목 → 실행 가능한 확인 명령의 사슬을 만들고, 채점기가 가짜 서비스를 여러 장애 모드로 띄워 그 사슬이 정확한 항목을 가리키는지 훈련으로 확인한다.
왜 중요한가
인계 문서는 쓴 날에만 맞다. 경보가 어떤 조건에서 울리는지, 울렸을 때 무엇으로 확인하는지가 코드와 따로 적혀 있으면 서비스가 바뀌는 순간 문서가 낡고, 그 사실은 새벽의 당번이 명령을 칠 때 드러난다. 그래서 경보 규칙은 스크립트가 읽는 데이터로 두고, 런북의 확인 줄은 종료 코드로 답하는 명령으로 쓰고, 그 명령이 실제로 도는지 점검기와 훈련으로 되풀이해 확인한다. 조용한 대시보드(지표를 못 긁음)를 정상으로 읽지 않는 것도 같은 사슬의 일부다.
예상 75분이다. 기본 60분 세션이 끝나기 전에 +시간으로 연장하자(최대 180분). 세션이 끝나면 /root/drill 의 파일은 사라지니, 남기고 싶은 코드는 종료 전에 따로 보관한다.
재료
- 인계 메모(경보 표·런북 규칙):
/opt/lab/drill/brief.md - 가짜 서비스:
python3 /opt/lab/drill/fakesvc.py --port 9311 --mode normal(--help로 장애 모드 확인) - 벤더가 남긴 옛 런북:
/opt/lab/drill/vendor-runbook.md - 채점기는 여러분이 띄운 서버를 쓰지 않는다. 빈 포트에 가짜 서비스를 여러 모드·여러 시드로 직접 띄워 여러분의 스크립트를 실행해 본다. 제공된 숫자를 외워 넣으면 통과하지 않는다.
단계
1. 가짜 서비스를 정상 모드로 띄우고 /metrics 응답을 가공 없이 /root/drill/normal.prom 에 저장한다. 원문의 # TYPE 줄에서 이름 형식 을 한 줄에 하나씩 /root/drill/families.txt 에 적는다.
2. /root/drill/promtext.py 를 쓴다. python3 promtext.py FILE 이 샘플마다 {"name","labels","value","timestamp"} 객체의 JSON 배열을 출력한다. 값은 숫자, 무한대·NaN 은 문자열 "+Inf"·"-Inf"·"NaN", 타임스탬프가 없으면 null. 주석·빈 줄은 건너뛰고, 레이블 값 안의 쉼표·중괄호와 \\·\"·\n 이스케이프를 올바르게 읽는다. 잘못된 샘플 줄이 하나라도 있으면 종료 코드 2.
3. 인계 메모의 여섯 경보를 /root/drill/alerts.json 에 옮긴다. {"alerts": [{"alert","severity","runbook","rule"}]} 형식이고 rule.kind 는 threshold(metric·op·value), ratio(metric·denominator·op·value), expires_within(metric·seconds), scrape, absent(metrics) 중 하나다. 채점기는 경계값을 포함한 여러 상황에서 여러분의 규칙을 판정해 본다.
4. /root/drill/diagnose.py 를 쓴다. python3 diagnose.py --url URL [--rules 경로](기본 /root/drill/alerts.json)가 URL/metrics 를 한 번 긁어 규칙으로 판정하고 {"target": URL, "firing": [{"alert","severity","runbook","labels"}]} 를 출력한다. labels 는 그 샘플의 레이블이다. 종료 코드는 경보가 없으면 0, 있으면 1. 문턱은 규칙 파일에서 읽는다.
5. /root/drill/diagnose.py 가 못 긁은 상황을 경보로 올리게 고친다. 연결 실패·HTTP 200 아님·형식 오류·orders_up 없음이면 OrdersTargetDown(labels {}) 하나만 내고 종료 코드 2. 필수 지표가 빠지면 빠진 지표마다 OrdersMetricAbsent(labels {"metric": 이름})를 낸다.
6. 여섯 경보의 런북 항목을 /root/drill/runbook.md 에 쓴다. 항목은 ## 런북이름 제목과 - 경보:·- 확인:·- 판단:·- 조치:·- 에스컬레이션: 다섯 줄이다. 확인 줄은 백틱으로 감싼 한 줄 명령이고 $ORDERS_URL 로 서비스를 가리키며, bash -o pipefail -c 로 돌렸을 때 정상이면 0, 그 장애면 0 이 아닌 값으로 곧바로 끝나야 한다.
7. /root/drill/check_runbook.py 를 쓴다. python3 check_runbook.py 런북.md --url URL 이 항목마다 확인 명령을 ORDERS_URL 환경 변수와 함께 bash -o pipefail -c 로 5초 제한 안에 돌리고, 런북 순서대로 [{"id","check","exit","status","reason"}] 를 출력한다. status 는 종료 코드 0 이면 ok, 아니면 broken. reason 은 ok·timeout·not-found(127)·exit N·no-check(확인 줄 없음). 하나라도 broken 이면 종료 코드 1. 벤더 런북에 돌려 고장 난 줄을 찾는다.
8. 당번 훈련 스크립트 /root/drill/oncall.sh 를 쓴다. bash oncall.sh URL 이 diagnose.py 로 경보를 얻고, 경보마다 runbook.md 의 항목을 찾아 확인 명령을 돌려 {"target": URL, "incidents": [{"alert","severity","runbook","labels","check_exit","confirmed","escalation"}]} 를 출력한다. confirmed 는 확인 명령이 0 이 아닌 값으로 끝났을 때(장애 확증) true, escalation 은 런북의 에스컬레이션 줄 그대로. 사건이 있으면 종료 코드 1, 없으면 0.
참고
- 원문 저장:
curl -fsS http://127.0.0.1:9311/metrics -o 파일— 파이프로 가공하면 마지막 줄바꿈이나 주석이 사라질 수 있다. - 서비스를 여러 개 띄워 비교할 때는 포트를 바꾼다(9312, 9313…). 끝난 서비스는
pkill -f 'fakesvc.py --port 9312'처럼 포트까지 지정해 내린다. - 흔한 실수 1: 레이블을 쉼표로 자르기.
note="handoff \"v2\", see RB"에서 깨진다. - 흔한 실수 2: 긁기 실패를 빈 목록으로 삼키기. 그러면 서비스가 죽은 밤에 경보가 0개로 나온다.
- 흔한 실수 3: 확인 명령에서
curl ... | grep -q. 첫 일치에서 grep 이 끝나 curl 이 SIGPIPE(종료 코드 23)를 받고, pipefail 아래에서 정상인데도 실패로 보일 수 있다.grep -c ... >/dev/null이나jq -e처럼 입력을 끝까지 읽는 형태를 쓴다. - 흔한 실수 4: 시간 제한을 bash 에만 걸기. 파이프 뒤의 curl 이 살아남아 점검기가 함께 멈춘다. 새 세션으로 띄우고 프로세스 그룹째 끊는다.
단계 8개
- 지표 원문을 그대로 긁어 두기
- 쉼표와 따옴표에 속지 않는 파서
- 인계 메모의 경보 여섯 개를 데이터로
- 한 번 긁어 규칙으로 판정하기
- 조용한 대시보드를 정상으로 읽지 않기
- 종료 코드로 답하는 런북
- 벤더 런북을 돌려 보고 고장 난 줄 찾기
- 경보부터 에스컬레이션까지 당번 훈련