--- name: guardia-deploy description: > GUARDiA 프로젝트에서 SSH/SFTP 기반 에이전트리스 배포 엔진 코드를 작성하거나 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: - paramiko 관련 코드 작성/수정 - 롤링 배포 로직 구현 - 헬스체크 / 롤백 로직 작성 - 배포 파이프라인 API 엔드포인트 작성 - 파일 무결성(MD5/SHA-256) 검증 코드 --- # GUARDiA 배포 엔진 스킬 ## 핵심 원칙 1. **백업 우선**: 배포 전 반드시 타임스탬프 포함 백업 (`app.jar.bak_YYYYMMDD_HHMMSS`) 2. **무결성 검증**: SFTP 전송 후 MD5 로컬 vs 원격 비교 필수 3. **헬스체크 강제**: 재기동 후 30초 이내 HTTP 200 미확인 시 즉시 롤백 4. **롤링 배포**: WAS #1 완료·검증 후 WAS #2 시작 (동시 배포 금지) 5. **감사 로그**: 성공/실패 모두 `log_execution()` 호출 필수 ## 환경 변수 ```python SSH_TIMEOUT = 300 # SSH 명령 실행 타임아웃 (초) HEALTH_TIMEOUT = 30 # 헬스체크 최대 대기 (초) HEALTH_INTERVAL = 2 # 헬스체크 폴링 간격 (초) DISK_THRESHOLD = 90 # 배포 전 디스크 임계치 (%) MAX_BATCH_SIZE = 50 # 티어2 병렬 배포 단위 ``` ## 금지 사항 - root 계정으로 SSH 접속 금지 → opsagent + sudoers 사용 - `rm -rf`, `mkfs`, `format`, `drop table` 등 Blacklist 명령 차단 - 자격증명(비밀번호) 로그 출력 금지 - 헬스체크 없이 다음 노드 진행 금지 - 라이선스 미등록/만료 상태에서 CICD 기능 실행 금지 (ENTERPRISE 에디션 전용) ## 코드 패턴 ### SSH 실행 ```python result = executor.execute_command("sh /app/scripts/startup.sh") if result["exit_code"] != 0: raise DeploymentError(f"startup 실패: {result['stderr']}") ``` ### 파일 전송 + 무결성 ```python remote_md5 = executor.upload_file(local_path, remote_path) local_md5 = hashlib.md5(open(local_path, 'rb').read()).hexdigest() assert local_md5 == remote_md5, "MD5 불일치" ``` ### 헬스체크 ```python deadline = time.time() + HEALTH_TIMEOUT while time.time() < deadline: try: r = requests.get(f"http://{ip}:8080", timeout=3) if r.status_code == 200: return True except: pass time.sleep(HEALTH_INTERVAL) return False # 실패 → 즉시 rollback 트리거 ``` ## 에러 처리 표준 ```python try: # 배포 로직 except DeploymentError as e: self._rollback(executor, node, backup_ts) log_execution(sr_id, node["ip"], "DEPLOY_FAILED", 1, str(e)) messenger.notify_failure(sr_id, str(e)) return False finally: executor.close() ``` ## 정적 vs 동적 자원 판별 ```python STATIC_EXTENSIONS = {".html", ".js", ".css", ".png", ".jpg", ".gif", ".svg"} DYNAMIC_EXTENSIONS = {".class", ".jar", ".war", ".properties"} def requires_restart(filename: str) -> bool: ext = Path(filename).suffix.lower() return ext in DYNAMIC_EXTENSIONS ``` ## 라이선스 확인 CICD/배포 기능은 ENTERPRISE 에디션 전용이다. 배포 시작 전 라이선스를 검증한다: ```python from routers.license import get_license_status async def verify_license_for_deploy(db): status = await get_license_status(db) if not status.get("valid"): raise DeploymentError(f"라이선스 오류: {status.get('message', '미등록')}") features = (status.get("limits") or {}).get("features", []) if "CICD" not in features: raise DeploymentError( f"CICD 기능은 ENTERPRISE 에디션에서만 사용 가능합니다 (현재: {status.get('edition')})" ) ```