콘텐츠로 이동

6. 실제로 되나요 — E2E 실행 결과

이 저장소의 결과는 주장이 아니라 채점 결과입니다. 장애를 주입하기 전에 정답(Ground truth)을 적어 두고, 에이전트의 결론을 아래 기준으로 채점했습니다. 에이전트가 틀린 부분도 그대로 기록합니다.

6.0 평가 방법

시간 지표

지표 계산
탐지 지연 경고 발화 − 장애 주입
인수 지연 조사 스레드 생성 − 경고 발화
근본 원인 도달 원인 확정 − 스레드 생성

RCA 점수 (10점)

항목 배점 기준
영향 범위 2 리소스·엔드포인트·리비전을 정확히 특정
직접 원인 3 Ground truth 와 일치
증거 2 로그·설정·메트릭 근거 제시
완화책 2 최소 범위이고 되돌릴 수 있음
불확실성 1 확인 못 한 부분을 구분해 표시

판정: ✅ Pass(8~10) · ⚠️ Partial(5~7) · ❌ Fail(0~4)

6.1 한눈에 보기

시나리오 장애 신호 탐지 인수 원인 도달 조치 점수 판정
S1 — 메모리 누수 → OOM HTTP 5xx 급증 3분 54초 4분 25초 재시작 + 1Gi→2Gi 10/10 ✅ Pass
S2 — 인그레스 포트 불일치 전 요청 503 2분 52초 49초 80초 targetPort 9090→8080 8/10 ✅ Pass
S3 — 주문 API 응답 지연 오류 없이 4초 지연 3분 44초 69초 4분 49초 미완료 — 수동 복구 8/10 ✅ Pass

S1 과 S2 는 증상이 같고(5xx) 원인이 다릅니다. S3 는 아예 오류가 나지 않습니다. S2 조사에서 에이전트가 남긴 문장이 이 Lab 의 핵심입니다.

"Root cause identified: Port mismatch, NOT OOM this time. … different root cause than previous incidents."

S3 는 원인을 정확히 지목했지만 완화를 끝내지 못했습니다. 감점 사유를 그대로 기록했습니다.

아래는 각 시나리오의 원본 기록입니다.

실행일: 2026-08-19 · 리전: eastus2 · 시나리오: 1 (IT 운영, GitHub 미연동) 모든 타임스탬프는 UTC 기준입니다.

시나리오 S1 — 무엇이 배포됐나요

azd up 으로 생성된 리소스 (리소스 그룹 rg-sre-lab):

리소스 이름 소요
Resource Group rg-sre-lab 6s
Application Insights appi-huvqg3bjooyw6 25s
Log Analytics workspace law-huvqg3bjooyw6 22s
Container Registry acrcagrubifyhuvqg3bjooyw6 10s
Container Apps Environment cae-huvqg3bjooyw6 58s
Container App (API) ca-grubify-huvqg3bjooyw6 17s
Container App (Frontend) ca-grubify-fe-huvqg3bjooyw6 —
SRE Agent sre-agent-huvqg3bjooyw6 —
Metric Alert alert-http-5xx-sre-lab (Sev3, 5xx > 5 / 5분, 평가 1분) —

scripts/post-provision.sh 결과: ACR 이미지 2종 빌드·배포, CORS 구성, Knowledge Base 4개 파일 색인, incident-handler(19 tools), Response Plan, Azure Monitor 연결 — 전부 성공.

위는 2026-08-19 최초 실행 시점의 기록입니다. 이후 S2·S3 시나리오를 만들며 경고 규칙 2개(alert-revision-unhealthy-sre-lab, alert-latency-sre-lab)와 지식 문서 lab-environment.md 를 추가했습니다. 지금 배포하면 경고 3개 · Knowledge Base 5개 파일이 만들어지고, 포털 기능 을 적용하면 지식 문서가 7개로 늘어납니다.

시나리오 S1 — 장애를 어떻게 주입했나요

Target:   https://ca-grubify-huvqg3bjooyw6...azurecontainerapps.io
Requests: 200 (interval 0.5s)

00:11 UTC  시작 — App is healthy (HTTP 200)
00:14 UTC  25/200 → 오류 0건
00:16 UTC  75/200 → 오류 2건   ← 최초 5xx 발생
00:17 UTC  100/200 → 오류 26건
00:19 UTC  200/200 → 오류 126건

Results: 74 successes, 126 errors out of 200 requests

6.3 에이전트 자동 조사 타임라인

Azure Monitor 경고 → Response Plan → incident-handler(Autonomous) 로 사람 개입 없이 진행된 실제 기록입니다.

시각(UTC) 이벤트
00:17:47 경고 alert-http-5xx-sre-lab (Sev3) 발화
00:18:41 에이전트가 경고 수신·확인(Acknowledged) 후 조사 시작
00:19:15 Knowledge Base 검색 — 유사 인시던트 3건 / 런북 4건 조회
00:19:30 Grubify 아키텍처 문서 · 인시던트 템플릿 · HTTP 500 런북 로드
00:19:38 Container Apps 진단 스킬 로드, 앱 정보·메트릭·로그·리비전 병렬 수집
00:20:57 분석: CPU 최대 1% (원인 아님), 메모리 3%→5%, 트래픽 32 req/min
00:22:06 근본 원인 확정 — System.OutOfMemoryException
00:22:20 완화 1 — 리비전 재시작 + 요청/메모리 상관 차트 생성
00:22:50 완화 2 — 메모리 1Gi → 2Gi 확장, 새 리비전 --0000003 생성
00:24:11 Azure Monitor 경고 종료(Close)
00:29:58 경고 상태 Resolved 로 전환 확인
00:30:05 인시던트 리포트 파일 생성 (incident-report-2026-08-19-oom-cart-api.md, 113줄)
00:31:37 Team Memory 저장 — debugging.md, overview.md, grubify-oom-cart-api-incident.md
00:32:11 최종 요약 보고 완료

측정 지표

항목 값
감지(MTTD) — 최초 5xx → 경고 발화 약 3분
완화(MTTR) — 경고 발화 → 메모리 확장 적용 약 5분
전체 조사 완료 — 경고 발화 → 최종 보고 약 14분
사람 개입 0회

조치를 실행한 주체 — 활동 로그 검증

"사람 개입 0회" 는 에이전트의 자기 보고가 아니라 Azure 활동 로그로 확인한 사실입니다.

시각(UTC) ARM 작업 호출자 결과
00:22:20 containerApps/revisions/restart/action id-sre-huvqg3bjooyw6 Succeeded
00:22:43 containerApps/listSecrets/action id-sre-huvqg3bjooyw6 Succeeded
00:22:43 → 00:22:59 containerApps/write id-sre-huvqg3bjooyw6 Succeeded

호출자는 사용자 계정(UPN)이 아니라 에이전트의 관리 ID (ManagedIdentity) 입니다. 즉 OBO 승인 경로가 아니었고, 사람이 버튼을 누른 것도 아닙니다 — 권한 구조에서 설명한대로 관리 ID 에 부여된 Container Apps Contributor 로 통과했습니다.

# 직접 확인하는 명령
az monitor activity-log list -g rg-sre-lab \
  --start-time 2026-08-19T00:00:00Z --end-time 2026-08-19T01:00:00Z \
  --query "[?contains(operationName.value,'Microsoft.App')].{time:eventTimestamp, op:operationName.value, caller:caller}" -o table

6.4 에이전트가 도출한 근본 원인

에이전트 최종 보고 원문(요약):

Root Cause System.OutOfMemoryException in CartController.AddItemToCart at /app/Controllers/CartController.cs:line 30. The in-memory cart store uses an unbounded dictionary with no eviction — under ~32 req/min, it exhausted the container's 1Gi memory limit, causing 25+ HTTP 500 failures across 8 Kestrel connections in a 40-second window.

즉, 파일명과 라인 번호 수준까지 원인을 특정했습니다. 근거로 Log Analytics 로그, 요청/메모리 상관 차트, 리비전 이력을 함께 제시했습니다.

6.5 완화 결과 검증

에이전트 조치 후 실제 상태:

Name                               Active  Replicas  Health   Memory  Cpu
ca-grubify-huvqg3bjooyw6--0000003  True    1         Healthy  2Gi     1.0
POST /api/cart/demo-user/items   → HTTP 200 (463ms)
GET  frontend /                  → HTTP 200 (700ms)

메모리 1Gi → 2Gi, CPU 0.5 → 1.0 으로 확장된 새 리비전이 정상 동작하며, 서비스가 복구되었습니다.

6.6 GitHub 미연동 시의 제약 (재현됨)

GITHUB_USER 를 설정하지 않고 실행했기 때문에, 에이전트는 GitHub 이슈 생성 단계에서 GitHub authorization failed. User must be logged in. 로 실패했습니다. 에이전트는 이를 스스로 인지하고 대체 경로(gh CLI → curl → 커넥터 설정 확인)를 시도한 뒤, 최종적으로 인시던트 리포트를 파일로 저장하고 사용자에게 GitHub 계정 연결을 요청했습니다.

시나리오 2·3 을 실행하려면 azd env set GITHUB_USER <username> 후 bash scripts/post-provision.sh --retry 를 실행하고, 출력되는 OAuth URL 에서 브라우저 승인이 필요합니다. (이 단계는 대화형이라 자동화되지 않습니다.)

6.7 실습 중 발견하고 수정한 이슈

원본 starter-lab 을 실제로 돌리면서 발견한 문제 3건을 이 저장소에서 수정했습니다.

# 증상 근본 원인 수정
1 incident-handler returned HTTP 400 — subagent 생성 실패 Windows 에서 command -v python3 가 Microsoft Store 별칭 스텁을 찾음. 스텁은 출력이 없고 stderr 안내문만 내보내는데, 스크립트가 2>&1 로 캡처해 그 텍스트를 JSON 대신 요청 본문으로 전송 scripts/post-provision.sh, scripts/setup.sh — 후보 인터프리터를 실제 실행해보고 검증하도록 변경
2 Response plan failed after 5 attempts (HTTP 400) 데이터플레인 API 가 maxAttempts 속성을 거부 (Unknown incident filter properties: maxAttempts). 서버가 maxAutomatedInvestigationAttempts 를 자동 설정 scripts/post-provision.sh — 페이로드에서 maxAttempts 제거
3 검증 섹션이 공백으로 출력 / 헬스체크가 항상 404 (a) 한국어 등 비 UTF-8 Windows 로캘에서 이모지 출력 실패 (b) 배포된 API 에 /health·/api/menu 라우트가 없음 (실제: /api/fooditems, /api/restaurants, /api/cart/{user}) PYTHONIOENCODING=utf-8 설정 + scripts/break-app.sh 헬스체크 경로 교정

추가로 subagent 생성 PUT 이 비동기(202) 라 연속 호출 시 일시적으로 400 이 나는 현상이 있어, post-provision.sh 의 subagent 생성에 재시도 로직을 넣었습니다.

6.8 시나리오 S2 — 인그레스 포트 불일치 (2026-08-20)

실행일: 2026-08-20 · 리전: eastus2 · 모드: Autonomous 시나리오 S1 과 증상은 같지만(HTTP 5xx) 원인이 다른 장애입니다.

Ground truth

인그레스의 targetPort 를 8080 → 9090 으로 바꿉니다. 앱은 그대로 8080 에서 듣고 있으므로 모든 요청이 엣지에서 실패합니다. 컨테이너 자체는 정상이라, 에이전트가 "앱이 죽었다"고 오판하지 않는지까지 확인합니다.

bash scripts/break-app-config.sh break     # 주입
bash scripts/break-app-config.sh restore   # 복구

타임라인

시각(UTC) 이벤트 Δ
05:48:57 targetPort 8080 → 9090 주입 —
05:49:39 첫 503 확인 (부하 90건 전부 503) +42초
05:51:49 경고 alert-revision-unhealthy-sre-lab (Sev2) 발화 +2분 52초
05:52:38 에이전트 인수 — 조사 스레드 생성 +49초
05:53:27 팀 메모리 검색 — 과거 OOM 인시던트 6건 회수 +49초
05:54:08 근본 원인 확정 +80초
05:54:53 자율 조치 — targetPort 9090 → 8080 +45초
05:55:22 복구 검증 시작
05:53:32 (별도) 5xx 메트릭 경고 발화 → 두 번째 조사 스레드

에이전트 분석

항목 결과
영향 범위 ca-grubify-huvqg3bjooyw6, 리비전 --0000005, 요청 67건 5xx — 정확
직접 원인 "The TargetPort 9090 does not match the listening port 8080" — 정확
증거 Pending:PortMismatch 시스템 로그, ReplicaUnhealthy(startup probe: connection refused), 컨테이너 콘솔의 바인딩 로그, 메모리·CPU 정상(1~2%)
완화책 targetPort 를 8080 으로 정렬 — 최소 범위이고 되돌릴 수 있음
오판 구분 "No OOM this time — different root cause than previous incidents" — 과거 인시던트에 끌려가지 않음

잘못된 주장 (감점 사유)

에이전트는 원인을 "새 리비전이 앱의 리슨 포트를 바꿨는데 인그레스를 안 맞췄다" 로 서술했습니다. 실제로는 반대입니다 — 앱은 계속 8080 이었고, 바뀐 것은 인그레스 설정입니다. 인과의 방향을 뒤집었습니다.

원인은 이 Lab 이 남긴 흔적이었습니다. 첫 주입 시도에서 ASPNETCORE_URLS=http://+:9090 을 설정했다가 효과가 없어 되돌렸는데(6.9 참고), 그 리비전 이력이 남아 에이전트가 "앱이 9090 을 듣다가 8080 으로 바뀌었다"고 추론할 근거를 만들었습니다. 조치는 정확했지만 서사는 틀렸습니다.

점수

영향 원인 증거 완화 불확실성 합계 판정
2 3 1 2 0 8/10 ✅ Pass

증거에서 감점 — 남아 있던 리비전 이력을 오독해 인과를 반대로 서술했습니다. 불확실성에서 감점 — 그 서술을 단정했고, 리비전 이력과 인그레스 변경 중 무엇이 원인인지 확인하지 못했다는 표시를 하지 않았습니다.

조치 주체 검증

2026-08-20T05:49:02Z  containerApps/write  admin@MngEnvMCAP359144.onmicrosoft.com   ← 사람(장애 주입)
2026-08-20T05:54:53Z  containerApps/write  25b6a2dc-...(id-sre-huvqg3bjooyw6)       ← 에이전트(자율 복구)

복구 결과: targetPort 8080, 리비전 --0000005 Healthy, GET /api/fooditems HTTP 200.

6.9 시나리오를 만들며 실패한 것

참고용으로 남깁니다. 처음 설계한 주입 방법은 동작하지 않았습니다.

시도 결과
ASPNETCORE_URLS=http://+:9090 으로 변경 ❌ 앱이 그대로 8080 을 바인딩 — 레플리카 정상, HTTP 200
인그레스 targetPort 를 9090 으로 변경 ✅ 전 요청 503

이 이미지는 환경 변수와 무관하게 8080 에 바인딩합니다. 그리고 실패한 시도가 남긴 리비전 이력이 위 6.8 의 오판을 유발했습니다 — 장애 주입 Lab 에서 증거 위생(evidence hygiene) 이 중요하다는 실제 사례입니다.

또 하나: 이 앱은 Application Insights 요청 계측이 없어 AppRequests 테이블이 비어 있습니다. 두 번째 조사에서 에이전트가 이를 스스로 발견하고 "App Insights and Log Analytics returned zero rows — need to sanity-check the data sources" 라고 말한 뒤 Container Apps 진단 도구로 경로를 바꿔 조사를 완료했습니다. 데이터 소스가 비어도 멈추지 않고 우회합니다.

6.10 시나리오 S3 — 응답 지연 (2026-08-20)

Ground truth: /api/orders 가 4초 지연 후 200 을 반환한다. 오류율은 변하지 않고 지연만 올라간다. 에이전트가 오류가 없는 장애를 인지하는지 확인.

시각(UTC) 사건 경과
06:32:37 ORDER_DELAY_MS=4000 주입 —
06:33:25 부하 시작 (10 동시 × 20회, 전부 200) +48초
06:34 ResponseTime 4034 ms 기록 (정상 4 ms)
06:36:21 경고 alert-latency-sre-lab(Sev2) 발화 +3분 44초
06:37:30 에이전트 인수 +69초
06:42:19 근본 원인 확정 — ORDER_DELAY_MS=4000 +4분 49초
06:43:30 pre-write-evidence-gate 훅 통과 — 이후 쓰기 미완료
06:46:03 사람이 수동 복구

점수 8/10 — 원인은 정확히 지목했지만 완화를 끝내지 못했습니다. 롤백 준비까지 가고 실행이 이어지지 않았고, 최종 요약은 "MITIGATED" 로 적었습니다. 실제로는 사람이 복구한 상태였습니다. 상세 채점은 guides/04-scenario-s3.md 에 있습니다.

이 시나리오를 만들 때 처음 시도한 방법은 실패했습니다

처음에는 CPU 를 굶겨서(1.0 → 0.25, maxReplicas 1) 지연을 만들려 했습니다. 결과는 실패였습니다.

구간 요청량(분당) 서버 응답시간(평균)
정상 (cpu 1.0) 120 1~3 ms
CPU 1/4 + 스케일아웃 차단 518 ~ 633 0 ~ 0.5 ms

트래픽은 분당 600건 이상 도달했지만 응답 시간은 그대로였습니다. GET /api/fooditems 가 메모리에서 정적 목록을 반환하는 수준이라 CPU 를 1/4 로 줄여도 병목이 생기지 않습니다. 중간에 관측된 72ms 는 리비전 교체 직후의 콜드 스타트였지 부하로 인한 지연이 아니었습니다.

그래서 앱에 지연 주입 지점을 직접 만드는 방식으로 바꿨습니다 — fork 한 저장소에 ORDER_DELAY_MS 미들웨어를 넣고 이미지를 다시 빌드했습니다. 이후 위 타임라인대로 재현에 성공했습니다.

교훈 — 부하로 지연을 만들려면 실제 작업을 하는 엔드포인트가 필요합니다. 정적 응답만 하는 API 는 CPU 를 굶겨도 느려지지 않습니다. 지연 시나리오는 부하보다 결정적인 주입 지점을 만드는 편이 재현성이 높습니다.

6.11 에이전트가 업스트림 저장소에 이슈를 만든 건

증상 — 에이전트가 조사 결과를 원본 저장소 dm-chelupati/grubify 에 이슈로 등록했습니다. 사용자의 fork 가 아니라 남의 저장소입니다.

원인 두 가지

# 원인 확인 방법
1 knowledge-base/grubify-architecture.md 가 저장소를 dm-chelupati/grubify 로 하드코딩 Knowledge Base 에 색인된 문서라 에이전트가 이를 정답으로 신뢰
2 GITHUB_USER 미설정 → post-provision.sh 의 예약 작업이 업스트림을 기본값으로 사용 azd env get-value GITHUB_USER → key not found

Code Access 에는 fork(daeungo1/grubify)가 정상 연결되어 있었습니다. 코드를 읽는 대상과 이슈를 쓰는 대상이 서로 달랐던 것이 핵심입니다.

조치 — 읽는 저장소와 쓰는 저장소를 분리

GITHUB_ISSUE_REPO 를 새로 도입해, 이슈를 쓸 저장소를 코드를 읽을 저장소와 따로 지정합니다.

azd env set GITHUB_ISSUE_REPO daeungo1/Azure-SRE-Agent-Lab   # 이슈가 쌓일 곳
azd env set GITHUB_REPO       daeungo1/grubify               # 코드를 읽을 곳
  • 지식 베이스에서 저장소 하드코딩을 제거하고, 실행 환경 값은 knowledge-base/lab-environment.md 에 배포 시점에 렌더링해 색인
  • 서브에이전트 지시문에 "오직 GITHUB_ISSUE_REPO 에만 쓸 것. 다른 저장소에는 이슈·코멘트·PR 금지. 코드 읽기는 허용" 을 명시
  • post-provision.sh 가 GITHUB_REPO 없이 예약 작업을 만들지 않도록 변경 (업스트림 기본값 제거)
  • yaml-to-api-json.py 가 빈 값이나 업스트림 저장소를 받으면 오류로 중단

재검증 (2026-08-20 07:21~07:34 UTC) — 같은 포트 불일치 장애를 다시 주입해 확인했습니다.

항목 결과
조사 스레드 58eb42e8 신규 생성 (07:28:24)
근본 원인 targetPort 9090 ↔ 앱 8080 불일치로 시작 프로브 크래시 루프
조치 UpdateTargetPort → 8080, 07:31 복구 (1/1 replica)
이슈 등록 위치 daeungo1/Azure-SRE-Agent-Lab#1 ✅
업스트림 신규 이슈 없음 ✅

daeungo1/Azure-SRE-Agent-Lab 저장소의 Issues 목록 화면입니다. 열린 이슈 1건으로 'Incident: HTTP 5xx due to TargetPort mismatch crash loop — 3rd recurrence (Grubify Container App)' 제목에 api-bug, bug, port-mismatch, recurring, severity-high 라벨이 붙어 있습니다.

에이전트가 직접 생성한 이슈 — 제목의 3rd recurrence 는 과거 두 번의 동일 장애를 메모리에서 회수해 붙인 것입니다.

교훈 — 에이전트에 쓰기 권한이 있는 외부 시스템에서는 대상 범위를 지식·설정 양쪽에서 고정해야 합니다. 권한을 좁히는 것만으로는 부족하고, 에이전트가 참조하는 문서가 잘못된 대상을 가리키면 그대로 따라갑니다.