--- name: manager-api description: > GUARDiA Manager 경량 FastAPI 백엔드를 구현하는 스킬. GUARDiA ITSM API가 커버하지 않는 시스템 수준 작업(서버 프로세스 관리, Nginx 제어, 배포 트리거, .env 설정 관리)을 담당한다. 트리거: Manager 백엔드 구현, 시스템 API 작성, 서비스 제어 API, 배포 이력 API, 설정 관리 API 요청 시. --- # GUARDiA Manager 백엔드 구현 스킬 ## 구현 범위 (GUARDiA ITSM과 역할 분담) **Manager Backend(8002)가 담당:** - 서버 리소스 조회 (psutil: CPU, 메모리, 디스크) - 서비스 시작/중지/재시작 (systemctl via subprocess) - Nginx 설정 테스트/리로드 - Deploy Webhook 트리거 (포트 9999 호출) - .env 파일 조회/수정 (보안: SECRET 키 마스킹) - Gitea API 프록시 (CORS 우회) **GUARDiA ITSM API(8001)에서 직접 호출 (재구현 금지):** - 사용자/인증, SR, 감사 로그, CMDB, 배포 이력, API Key 관리 등 ## 프로젝트 구조 ``` backend/ ├── main.py ├── requirements.txt ├── core/ │ └── auth.py ← GUARDiA ITSM JWT 검증 ├── routers/ │ ├── system.py ← 서버 상태, 서비스 제어 │ ├── deploy.py ← 배포 트리거, Gitea 연동 │ └── config.py ← .env, Nginx 설정 └── .env ``` ## main.py 기본 구조 ```python from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import os app = FastAPI(title="GUARDiA Manager API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=[os.environ.get("MANAGER_ALLOWED_ORIGINS", "http://localhost:5173")], allow_methods=["*"], allow_headers=["*"], allow_credentials=True, ) from routers import system, deploy, config app.include_router(system.router, prefix="/api/system", tags=["system"]) app.include_router(deploy.router, prefix="/api/deploy", tags=["deploy"]) app.include_router(config.router, prefix="/api/config", tags=["config"]) @app.get("/health") async def health(): return {"status": "ok", "service": "guardia-manager"} ``` ## routers/system.py — 서버 상태 및 서비스 제어 ```python import psutil, subprocess from fastapi import APIRouter, Depends, HTTPException from core.auth import verify_guardia_token router = APIRouter() ALLOWED_SERVICES = { # 허용된 서비스만 제어 가능 "nginx", "zioinfo", "guardia", "gitea", "jenkins", "postgresql", "ollama", "zioinfo-deploy" } @router.get("/resources") async def get_resources(user=Depends(verify_guardia_token)): """CPU/메모리/디스크 현황""" return { "cpu_percent": psutil.cpu_percent(interval=0.1), "memory": { "total_gb": round(psutil.virtual_memory().total / 1e9, 1), "used_gb": round(psutil.virtual_memory().used / 1e9, 1), "percent": psutil.virtual_memory().percent, }, "disk": { "total_gb": round(psutil.disk_usage('/').total / 1e9, 1), "used_gb": round(psutil.disk_usage('/').used / 1e9, 1), "percent": psutil.disk_usage('/').percent, }, } @router.get("/services") async def list_services(user=Depends(verify_guardia_token)): """허용된 서비스 상태 목록""" result = {} for svc in ALLOWED_SERVICES: proc = subprocess.run( ["systemctl", "is-active", svc], capture_output=True, text=True ) result[svc] = proc.stdout.strip() return result @router.post("/services/{name}/restart") async def restart_service(name: str, user=Depends(verify_guardia_token)): """서비스 재시작 (admin 역할 필요)""" if user.get("role") != "admin": raise HTTPException(status_code=403, detail="admin 권한 필요") if name not in ALLOWED_SERVICES: raise HTTPException(status_code=400, detail="허용되지 않은 서비스") result = subprocess.run( ["systemctl", "restart", name], capture_output=True, text=True ) if result.returncode != 0: raise HTTPException(status_code=500, detail=result.stderr[:200]) return {"message": f"{name} 재시작 완료"} ``` ## routers/deploy.py — 배포 트리거 ```python import httpx from fastapi import APIRouter, Depends from core.auth import verify_guardia_token router = APIRouter() DEPLOY_WEBHOOK = "http://localhost:9999/" @router.post("/trigger/{repo}") async def trigger_deploy(repo: str, user=Depends(verify_guardia_token)): """Deploy Webhook 수동 트리거""" async with httpx.AsyncClient() as client: resp = await client.post(DEPLOY_WEBHOOK, json={"repo": repo, "triggered_by": user.get("sub")}, timeout=10) return {"status": resp.status_code, "message": "배포 트리거됨"} @router.get("/history") async def deploy_history(user=Depends(verify_guardia_token)): """배포 로그 마지막 100줄""" try: with open("/var/log/zioinfo/deploy.log") as f: lines = f.readlines()[-100:] return {"lines": lines} except FileNotFoundError: return {"lines": []} ``` ## routers/config.py — 설정 관리 ```python import os, re from fastapi import APIRouter, Depends, HTTPException from core.auth import verify_guardia_token router = APIRouter() ENV_PATH = "/opt/guardia/app/.env" SENSITIVE_KEYS = {"SECRET", "PASSWORD", "KEY", "TOKEN", "PASS"} def mask_sensitive(key: str, value: str) -> str: """보안 키 값 마스킹""" if any(s in key.upper() for s in SENSITIVE_KEYS): return "****" return value @router.get("/env") async def get_env(user=Depends(verify_guardia_token)): """환경변수 목록 (보안 값 마스킹)""" if user.get("role") != "admin": raise HTTPException(status_code=403, detail="admin 권한 필요") result = {} try: with open(ENV_PATH) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: key, _, val = line.partition("=") result[key.strip()] = mask_sensitive(key.strip(), val.strip()) except FileNotFoundError: pass return result @router.post("/nginx/reload") async def nginx_reload(user=Depends(verify_guardia_token)): """Nginx 설정 테스트 후 리로드""" import subprocess test = subprocess.run(["nginx", "-t"], capture_output=True, text=True) if test.returncode != 0: raise HTTPException(status_code=400, detail=f"Nginx 설정 오류: {test.stderr[:200]}") subprocess.run(["systemctl", "reload", "nginx"]) return {"message": "Nginx 리로드 완료"} ``` ## requirements.txt ```txt fastapi==0.115.0 uvicorn[standard]==0.30.0 httpx==0.27.0 psutil==5.9.8 python-jose[cryptography]==3.3.0 python-dotenv==1.0.1 ``` ## .env (Manager Backend) ```env MANAGER_PORT=8002 GUARDIA_API=http://localhost:8001 GUARDIA_JWT_SECRET=guardia-jwt-production-secret-2026-please-change MANAGER_ALLOWED_ORIGINS=http://localhost:5173,http://zioinfo.co.kr:8080 ```