G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
244 lines
7.4 KiB
Markdown
244 lines
7.4 KiB
Markdown
# GUARDiA ITSM — Priority 5: 외부 시스템 연동 고도화
|
|
|
|
**문서 버전**: 1.0 | **작성일**: 2026-05-25
|
|
|
|
---
|
|
|
|
## 1. Jenkins 운영 배포 승인 연동
|
|
|
|
### 목적
|
|
Jenkins Declarative Pipeline의 `input()` 단계와 ITSM 승인 API를 양방향으로 연동한다.
|
|
운영(prd) 배포 시 ITSM에서 PM이 승인해야만 Jenkins 파이프라인이 진행된다.
|
|
|
|
### 흐름
|
|
|
|
```
|
|
GUARDiA Vibe 세션 → "배포(prd)" 버튼 클릭
|
|
│
|
|
▼ POST /api/vibe/{id}/deploy { environment: "prd" }
|
|
│
|
|
▼ Jenkins Job 트리거 (core/cicd.py)
|
|
│
|
|
Jenkins Pipeline:
|
|
stage('Request ITSM Approval') {
|
|
steps {
|
|
sh """
|
|
curl -X POST ${ITSM_URL}/api/vibe/${SESSION_ID}/request-approval \
|
|
-H "Authorization: Bearer ${ITSM_TOKEN}" \
|
|
-d '{"environment": "prd", "build_number": ${BUILD_NUMBER}}'
|
|
"""
|
|
}
|
|
}
|
|
stage('Wait for ITSM Approval') {
|
|
steps {
|
|
timeout(time: 60, unit: 'MINUTES') {
|
|
waitUntil {
|
|
def result = sh(
|
|
script: "curl -s ${ITSM_URL}/api/vibe/${SESSION_ID}/approval-status",
|
|
returnStdout: true
|
|
).trim()
|
|
return result == '"APPROVED"'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stage('Deploy to Production') { ... }
|
|
```
|
|
|
|
### ITSM API 신규 엔드포인트
|
|
|
|
```
|
|
POST /api/vibe/{id}/request-approval
|
|
→ VibeDeploy 승인 요청 생성 (SRApproval 또는 AgentApproval 재사용)
|
|
→ PM에게 메신저 알림
|
|
|
|
GET /api/vibe/{id}/approval-status
|
|
→ "PENDING" | "APPROVED" | "REJECTED"
|
|
|
|
PATCH /api/vibe/{id}/approve
|
|
→ PM이 ITSM UI에서 승인 처리
|
|
→ Jenkins 폴링 응답에 "APPROVED" 반환
|
|
```
|
|
|
|
---
|
|
|
|
## 2. SonarQube Quality Gate 연동
|
|
|
|
### 목적
|
|
Jenkins 빌드 완료 후 SonarQube 분석 결과를 ITSM에 자동 등록한다.
|
|
Quality Gate 실패 시 SR을 생성하여 개발팀에 통보한다.
|
|
|
|
### core/cicd.py 추가 함수
|
|
|
|
```python
|
|
async def get_sonarqube_result(project_key: str) -> SonarResult:
|
|
"""SonarQube Quality Gate 결과 조회"""
|
|
url = f"{SONARQUBE_URL}/api/qualitygates/project_status?projectKey={project_key}"
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(url, headers={"Authorization": f"Bearer {SONARQUBE_TOKEN}"})
|
|
data = r.json()
|
|
status = data["projectStatus"]["status"] # OK | WARN | ERROR
|
|
|
|
metrics = {
|
|
c["metricKey"]: c["value"]
|
|
for c in data["projectStatus"]["conditions"]
|
|
}
|
|
return SonarResult(
|
|
status=status,
|
|
coverage=metrics.get("coverage", "N/A"),
|
|
bugs=metrics.get("bugs", "0"),
|
|
vulnerabilities=metrics.get("vulnerabilities", "0"),
|
|
code_smells=metrics.get("code_smells", "0"),
|
|
)
|
|
|
|
async def handle_sonar_gate_failure(session_id: int, result: SonarResult, db: AsyncSession):
|
|
"""Quality Gate 실패 시 SR 자동 생성"""
|
|
if result.status == "ERROR":
|
|
sr = SRRequest(
|
|
title=f"SonarQube Quality Gate 실패 — 세션 {session_id}",
|
|
description=f"취약점: {result.vulnerabilities}, 버그: {result.bugs}, 커버리지: {result.coverage}%",
|
|
sr_type="DEPLOY",
|
|
priority="HIGH",
|
|
)
|
|
db.add(sr)
|
|
await db.commit()
|
|
```
|
|
|
|
### Jenkins 연동 (Jenkinsfile에 추가)
|
|
|
|
```groovy
|
|
stage('SonarQube Analysis') {
|
|
steps {
|
|
withSonarQubeEnv('SonarQube') {
|
|
sh 'mvn sonar:sonar -Dsonar.projectKey=${PROJECT_KEY}'
|
|
}
|
|
}
|
|
}
|
|
stage('Quality Gate') {
|
|
steps {
|
|
timeout(time: 5, unit: 'MINUTES') {
|
|
waitForQualityGate abortPipeline: false
|
|
}
|
|
// ITSM에 결과 전송
|
|
sh """
|
|
curl -X POST ${ITSM_URL}/api/vibe/sonar-result \
|
|
-d '{"session_id": ${SESSION_ID}, "project_key": "${PROJECT_KEY}"}'
|
|
"""
|
|
}
|
|
}
|
|
```
|
|
|
|
### 환경 변수
|
|
```bash
|
|
SONARQUBE_URL=http://sonar.agency.go.kr:9000
|
|
SONARQUBE_TOKEN=<SonarQube API Token>
|
|
```
|
|
|
|
---
|
|
|
|
## 3. SSL 자동 갱신 (Let's Encrypt)
|
|
|
|
### 목적
|
|
certbot과 연동하여 SSL 인증서를 자동으로 갱신하고, 결과를 ITSM에 기록한다.
|
|
|
|
### scripts/sm/ssl/ssl_auto_renew.sh
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# SSL 자동 갱신 스크립트 (서버에서 실행)
|
|
set -euo pipefail
|
|
|
|
ITSM_URL="${ITSM_URL:-http://localhost:8001}"
|
|
SERVER_ID="${SERVER_ID:-}"
|
|
ITSM_TOKEN="${ITSM_TOKEN:-}"
|
|
|
|
# certbot 갱신
|
|
if certbot renew --quiet --non-interactive \
|
|
--deploy-hook "systemctl reload nginx 2>/dev/null || systemctl reload apache2 2>/dev/null"; then
|
|
|
|
# 새 만료일 조회
|
|
NEW_EXPIRE=$(openssl x509 -in /etc/letsencrypt/live/*/cert.pem -noout -enddate \
|
|
| cut -d= -f2)
|
|
NEW_EXPIRE_ISO=$(date -d "$NEW_EXPIRE" +"%Y-%m-%d")
|
|
|
|
# ITSM에 갱신 기록
|
|
curl -s -X POST "${ITSM_URL}/api/ssl/renew/${SERVER_ID}" \
|
|
-H "Authorization: Bearer ${ITSM_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"new_expire_date\": \"${NEW_EXPIRE_ISO}\", \"renewed_by\": \"certbot-auto\", \"notes\": \"Let's Encrypt 자동 갱신\"}"
|
|
|
|
echo "[OK] SSL 갱신 완료: ${NEW_EXPIRE_ISO}"
|
|
else
|
|
echo "[WARN] SSL 갱신 불필요 (아직 유효기간 충분)"
|
|
fi
|
|
```
|
|
|
|
### 배치 작업 등록
|
|
|
|
`tb_batch_job`에 자동 갱신 배치 잡 등록:
|
|
- `cron_expr`: `0 3 * * *` (매일 03:00)
|
|
- `command`: `SSL_WATCHER=true bash /opt/guardia/scripts/ssl/ssl_auto_renew.sh`
|
|
- `alert_on_fail`: true
|
|
|
|
---
|
|
|
|
## 4. SSE 스트리밍 확장
|
|
|
|
### 목적
|
|
배치 실행 로그와 빌드 로그를 SSE로 실시간 스트리밍한다.
|
|
|
|
### core/events.py 확장
|
|
|
|
```python
|
|
# 배치 실행 로그 스트리밍
|
|
async def stream_batch_log(run_id: int) -> AsyncGenerator[str, None]:
|
|
"""tb_batch_run.stdout 청크 단위 SSE 전송"""
|
|
last_len = 0
|
|
for _ in range(300): # 최대 5분 (1초 간격)
|
|
async with SessionLocal() as db:
|
|
run = await db.get(BatchRun, run_id)
|
|
if run.stdout and len(run.stdout) > last_len:
|
|
new_data = run.stdout[last_len:]
|
|
last_len = len(run.stdout)
|
|
yield f"data: {json.dumps({'chunk': new_data})}\n\n"
|
|
if run.status not in ("RUNNING",):
|
|
yield f"data: {json.dumps({'done': True, 'status': run.status})}\n\n"
|
|
break
|
|
await asyncio.sleep(1)
|
|
|
|
# Jenkins 빌드 로그 스트리밍
|
|
async def stream_build_log(session_id: int) -> AsyncGenerator[str, None]:
|
|
"""Jenkins Build Log API 폴링 → SSE 전송"""
|
|
# GET {JENKINS_URL}/job/{name}/{number}/logText/progressiveText?start=0
|
|
offset = 0
|
|
while True:
|
|
log_chunk, next_offset = await cicd.get_progressive_log(session_id, offset)
|
|
if log_chunk:
|
|
yield f"data: {json.dumps({'chunk': log_chunk, 'offset': next_offset})}\n\n"
|
|
offset = next_offset
|
|
done = await cicd.is_build_complete(session_id)
|
|
if done:
|
|
yield f"data: {json.dumps({'done': True})}\n\n"
|
|
break
|
|
await asyncio.sleep(2)
|
|
```
|
|
|
|
### 신규 엔드포인트
|
|
|
|
```
|
|
GET /api/batch/runs/{run_id}/stream 배치 로그 실시간 스트리밍 (SSE)
|
|
GET /api/vibe/{id}/build/stream 빌드 로그 실시간 스트리밍 (SSE)
|
|
```
|
|
|
|
### 프론트엔드 연결
|
|
|
|
```javascript
|
|
// batch.html — 실행 로그 스트리밍
|
|
const es = new EventSource(`/api/batch/runs/${runId}/stream?token=${token}`);
|
|
es.onmessage = (e) => {
|
|
const data = JSON.parse(e.data);
|
|
if (data.chunk) logBox.textContent += data.chunk;
|
|
if (data.done) { es.close(); updateRunStatus(data.status); }
|
|
};
|
|
```
|