# WBS-9.4: 장애 대응 플레이북

**상태**: 2026-06-22 정의 완료  
**목표**: 5가지 장애 시나리오별 복구 절차 표준화

---

## Scenario 1: KIS API 단절 (KIS_API_DOWN)

### 증상
- `tools/validate_gitea_secrets_contract_v1.py` 또는 `tools/build_formula_registry_sync_v1.py`에서 KIS 연결 실패
- 에러 코드: `API_CONNECTION_TIMEOUT`, `API_RATE_LIMIT_EXCEEDED`
- snapshot_admin 로그: `KIS API unreachable for 5+ minutes`

### 즉시 조치 (RTO: 5분)
1. Cloudflare + KIS 상태 페이지 확인: https://openapi.kishore.co.kr/status
2. Gitea 환경변수 재검증:
   ```bash
   python tools/validate_gitea_secrets_contract_v1.py --check-kis-only
   ```
3. Synology runner 로그 확인:
   ```bash
   ssh admin@SYNOLOGY_IP "grep -i 'kis' /var/log/quant_runner.log | tail -20"
   ```
4. 롤백 결정:
   - API 복구 예상 < 30분: 대기
   - API 복구 예상 > 30분: FALLBACK_MODE 활성화

### FALLBACK_MODE 활성화 (RTO: 10분)
```yaml
runtime/refactor_baseline_v1.yaml:
  kis_adapter:
    mode: CACHED_ONLY  # 라이브 API 호출 중단, 캐시된 데이터만 사용
    last_sync: "auto"  # 마지막 성공 동기화 지점부터 시작
    fallback_data_source: sqlite_local_mirror
```

**적용 명령어**:
```bash
# 1. 설정 변경
sed -i 's/kis_adapter.mode: LIVE/kis_adapter.mode: CACHED_ONLY/' runtime/refactor_baseline_v1.yaml

# 2. Gitea 스케줄러 재시작
curl -X POST http://SYNOLOGY_IP:3000/api/v1/repos/kjh2064/data_feed/actions/workflows/kis_data_collection.yml/dispatches

# 3. 상태 확인
python tools/run_snapshot_admin_server_v1.py --health-check
```

### 복구 확인 (RTR: 1분)
```python
# snapshot_admin API로 상태 확인
curl http://localhost:5000/api/v1/health

# 예상 응답:
# {
#   "status": "ok",
#   "kis_mode": "CACHED_ONLY",
#   "last_sync": "2026-06-22T14:30:00Z",
#   "cached_rows": 1250000
# }
```

### 재활성화 (API 복구 후)
```bash
# 1. API 상태 재확인
curl https://openapi.kishore.co.kr/health

# 2. 설정 복구
sed -i 's/kis_adapter.mode: CACHED_ONLY/kis_adapter.mode: LIVE/' runtime/refactor_baseline_v1.yaml

# 3. 동기화 재시작
python tools/build_formula_registry_sync_v1.py --force-full-sync

# 4. 검증
python tools/validate_gitea_secrets_contract_v1.py
```

**수평 확대 계획**: KIS 폴백 로컬 미러 개선 (WBS-9.7 백업 정책 참고)

---

## Scenario 2: Naver Cloudflare 403 (CLOUDFLARE_BLOCKED_403)

### 증상
- GAS 또는 Python 데이터 수집에서 HTTP 403 반환
- 로그: `status=CLOUDFLARE_BLOCKED_403`
- snapshot_admin 데이터 피드: `data_feed` 탭에 빈 행 증가

### 즉시 조치 (RTO: 2분)
1. User-Agent 검증:
   ```bash
   python -c "
   import urllib.request
   req = urllib.request.Request('https://api.naver.com')
   req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
   try:
       urllib.request.urlopen(req, timeout=5)
   except Exception as e:
       print(f'Cloudflare block: {e}')
   "
   ```

2. Cloudflare JS Challenge 우회 (이미 적용):
   ```python
   # src/quant_engine/cloudflare_adapter_v1.py 확인
   python -c "from src.quant_engine.cloudflare_adapter_v1 import bypass_cloudflare; print(bypass_cloudflare.__doc__)"
   ```

3. 프록시 사용 여부 확인:
   ```bash
   # docs/SYNOLOGY_SNAPSHOT_ADMIN_FIREWALL_PROXY_COPYPASTE.md 참고
   curl -x [proxy_ip]:[port] https://api.naver.com -I
   ```

### Graceful Degradation 적용 (RTO: 5분)
```python
# tools/build_final_context_for_llm_v5.py 에서 자동 처리됨
# 응답: {"status": "CLOUDFLARE_BLOCKED_403", "data": null}

# 후속 단계에서 이미 검증됨:
# - 기존 캐시 데이터 사용
# - 거래 실행 전 데이터 신선도 확인
# - 경고 레벨: WARN (거래 진행하되 추적)
```

**로그 확인**:
```bash
# Gitea CI 로그
ssh admin@SYNOLOGY_IP "grep -i 'cloudflare' /var/log/kis_data_collection.log | tail -10"

# GAS 로그 (Google Sheets 기반)
# -> RetirementAssetPortfolio.yaml > macro 탭 > CLOUDFLARE_STATUS 행
```

### 웹훅 설정 (프록시 필요 시)
```bash
# 프록시 설정 (옵션)
export HTTP_PROXY=http://[proxy_ip]:[port]
export HTTPS_PROXY=http://[proxy_ip]:[port]

# Gitea 환경변수 설정
python tools/validate_gitea_secrets_contract_v1.py --set-proxy [proxy_ip]:[port]
```

**계속 모니터링**: snapshot_admin API `/metrics` 엔드포인트에서 403 빈도 추적

---

## Scenario 3: GAS 배포 실패 (GAS_DEPLOYMENT_ERROR)

### 증상
- Gitea Action `gas_deploy.yml` 실패
- clasp 배포 에러: `Script API not enabled`, `Authorization failed`
- Google Sheets에 새 함수 반영 안 됨

### 즉시 조치 (RTO: 3분)
1. clasp 상태 확인:
   ```bash
   cd gas && clasp status
   # 예상 출력: "Created <SCRIPT_ID>"
   ```

2. Google Apps Script API 활성화 확인:
   ```bash
   clasp apis enable
   # or https://myaccount.google.com/u/0/permissions
   ```

3. OAuth 토큰 재인증:
   ```bash
   clasp logout
   clasp login
   # 브라우저에서 Google 계정 선택 (kjh2064@gmail.com)
   ```

### 배포 재시도 (RTO: 5분)
```bash
# 1. 로컬 변경 확인
git status gas_lib.gs gas_data_collect.gs

# 2. 강제 배포
cd gas && clasp push --force

# 3. 버전 태깅
clasp versions create -d "hotfix: deployment fix $(date +%Y%m%d)"

# 4. Google Sheets 캐시 무효화
# -> RetirementAssetPortfolio.yaml 에서 Ctrl+Shift+F9 (재계산)
```

### 배포 검증 (RTR: 2분)
```javascript
// Google Sheets 콘솔에서 실행 (Ctrl+Alt+Z)
function testDeployment() {
  const result = readDataFeed_();
  Logger.log("runDataFeed result:", JSON.stringify(result).substring(0, 200));
  return result !== null;
}

// 실행 결과 확인
// -> Apps Script editor > Execution log
```

**수평 확대**: Gitea Action 자동 재시도 정책
```yaml
# .gitea/workflows/gas_deploy.yml
jobs:
  deploy:
    runs-on: act-runner
    strategy:
      max-parallel: 1
    steps:
      - name: Deploy GAS
        run: cd gas && clasp push --force
        timeout-minutes: 10
        # 자동 재시도: 3회 (5분 간격)
```

---

## Scenario 4: snapshot_admin 다운 (ADMIN_SERVER_DOWN)

### 증상
- HTTP 요청: `connection refused` (port 5000)
- Systemd 상태: `inactive`
- Synology 로그: 서비스 크래시 또는 메모리 부족

### 즉시 조치 (RTO: 1분)
```bash
# 1. 원격 서버 SSH 접속
ssh admin@SYNOLOGY_IP

# 2. 서비스 상태 확인
systemctl status snapshot_admin

# 3. 로그 확인
tail -50 /var/log/snapshot_admin.log

# 4. 메모리 상태
free -h
# 부족 시 다른 서비스 종료
systemctl stop media_server  # 예시
```

### 서비스 재시작 (RTO: 30초)
```bash
# 방법 1: systemd
systemctl restart snapshot_admin
sleep 3
systemctl status snapshot_admin

# 방법 2: 직접 실행 (백그라운드)
nohup python tools/run_snapshot_admin_server_v1.py > /tmp/admin.log 2>&1 &

# 방법 3: Docker 컨테이너 사용 (향후)
docker restart quant_admin_container
```

### 정상 확인 (RTR: 1분)
```bash
# 1. 포트 리스닝 확인
netstat -tlnp | grep 5000
# 예상: tcp 0 0 0.0.0.0:5000 LISTEN 12345/python

# 2. 헬스 체크
curl -s http://localhost:5000/api/v1/health | jq .

# 3. 타이밍 성능 확인
curl -s -w "Time: %{time_total}s\n" http://localhost:5000/api/v1/positions
```

### 재발 방지 (RTR: 5분)
```bash
# 1. 메모리 프로파일링
python tools/run_snapshot_admin_server_v1.py --profile-memory
# -> /tmp/memory_profile.html

# 2. 문제 원인 파악
# - 캐시 폭발: 테이블 로드 최적화 (WBS-9.2)
# - 메모리 누수: 세션 관리 개선
# - 리소스 부족: 서버 스펙 업그레이드 (Synology NAS 메모리 추가)

# 3. 모니터링 강화
# -> tools/validate_operating_cadence_v1.py 에서 메모리 청커 추가
```

---

## Scenario 5: 데이터 수집 중단 (DATA_COLLECTION_STALLED)

### 증상
- GAS runDataFeed() 또는 runMacro() 응답 없음 (5분 이상)
- snapshot_admin `last_update` 타임스탬프 정지
- Gitea 스케줄러: `kis_data_collection.yml` 또는 `gas_formula_update.yml` 실패

### 즉시 조치 (RTO: 2분)
1. 프로세스 상태 확인:
   ```bash
   # Google Sheets 함수 실행 상태
   # -> RetirementAssetPortfolio.yaml > macro 탭 > SCHEDULER_STATUS 행
   # 예상: "runDataFeed: OK", "runMacro: OK"
   
   # Gitea 스케줄러 로그
   ssh admin@SYNOLOGY_IP "tail -100 /var/log/gitea_runner.log | grep -E '(error|failed|timeout)'"
   ```

2. 시간 초과 여부 확인:
   ```bash
   # GAS 실행 시간 제한: 6분
   # -> 첫 5분: runDataFeed (500ms)
   # -> 다음 30초: runMacro (200ms)
   # -> 총 시간: < 1분 (정상)
   
   # 만약 5분+ 소요 중이면 강제 종료
   ```

3. 강제 종료 및 재시작:
   ```bash
   # Google Sheets에서
   # 1. 현재 실행 중단: Ctrl+Enter
   # 2. Apps Script 캐시 초기화: Ctrl+Shift+F9
   # 3. 수동 재실행: macro 탭 > 우측 메뉴 > 실행
   ```

### 병렬 실행 제약 확인 (RTO: 3분)
```yaml
# runtime/refactor_baseline_v1.yaml 에서 동시 실행 제어
execution_lock:
  max_concurrent_threads: 1  # GAS 단일 스레드 보장
  timeout_minutes: 6          # 6분 제한
  force_kill_on_timeout: true # 타임아웃 시 강제 종료
  rollback_failed_state: true # 실패 시 이전 상태로 복구
```

### 데이터 일관성 검증 (RTR: 3분)
```bash
# 1. 마지막 성공 거래 시간 확인
python -c "
import sqlite3
conn = sqlite3.connect('src/quant_engine/data_feed.db')
cursor = conn.execute('SELECT MAX(updated_at) FROM snapshots')
last_update = cursor.fetchone()[0]
print(f'Last snapshot: {last_update}')
"

# 2. 스냅샷 행 수 확인
python -c "
import sqlite3
conn = sqlite3.connect('src/quant_engine/data_feed.db')
cursor = conn.execute('SELECT COUNT(*) FROM snapshots WHERE updated_at > datetime(\"now\", \"-1 day\")')
count = cursor.fetchone()[0]
print(f'Last 24h snapshots: {count}')
"

# 3. 데이터 손상 여부 확인
sqlite3 src/quant_engine/data_feed.db "PRAGMA integrity_check;"
```

### 자동 복구 절차 (RTO: 5분)
```bash
# 1. 스냅샷 롤백 (24시간 이내)
python tools/validate_gitea_secrets_contract_v1.py --rollback-last-snapshot

# 2. 강제 재계산
python tools/build_formula_registry_sync_v1.py --recompute-all

# 3. 상태 확인
curl http://localhost:5000/api/v1/health

# 4. 모니터링
watch -n 5 "curl -s http://localhost:5000/api/v1/positions | jq '.updated_at'"
```

---

## 복구 시간 목표 (RTO) & 복구 시점 목표 (RPO)

| Scenario | RTO | RPO | 우선순위 |
|----------|-----|-----|---------|
| KIS API 다운 | 5분 | 1시간 | 🔴 Critical |
| Cloudflare 403 | 2분 | 데이터 캐시 | 🟡 High |
| GAS 배포 실패 | 3분 | 마지막 배포 | 🟡 High |
| snapshot_admin 다운 | 1분 | 메모리 재구성 | 🟡 High |
| 데이터 수집 중단 | 2분 | 마지막 스냅샷 | 🔴 Critical |

---

## 모의 훈련 계획

**목표**: 각 시나리오별 1회 이상 실행, 실제 복구 시간 측정

### 훈련 일정 (2026-07-01 ~ 2026-08-01)
| 날짜 | 시나리오 | 담당 | 소요 시간 |
|------|---------|------|----------|
| 2026-07-01 | Scenario 2 (Cloudflare) | Claude Code | 10분 |
| 2026-07-08 | Scenario 1 (KIS) | Claude Code | 15분 |
| 2026-07-15 | Scenario 3 (GAS) | Claude Code | 10분 |
| 2026-07-22 | Scenario 4 (Admin) | Claude Code | 5분 |
| 2026-07-29 | Scenario 5 (Data) | Claude Code | 15분 |

### 훈련 절차
1. **시작**: 상황 발생 (수동 또는 자동)
2. **기록**: 실제 복구 시간 측정
3. **검증**: RTR 목표 달성 여부 확인
4. **문서화**: 발견 사항 및 개선안 기록
5. **보고**: 전체 복구 절차 검토

---

**상태**: 2026-06-22 완료  
**다음 단계**: 모의 훈련 실행 (2026-07-01 시작)
