guardia-docs/13_확장개발_Priority5_외부연동.md
DESKTOP-TKLFCPRython 938b25f286 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
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>
2026-05-29 18:18:52 +09:00

7.4 KiB

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 추가 함수

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에 추가)

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}"}'
        """
    }
}

환경 변수

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

#!/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 확장

# 배치 실행 로그 스트리밍
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)

프론트엔드 연결

// 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); }
};