zioinfo-mail/skills/guardia-deploy/SKILL.md
DESKTOP-TKLFCPR\ython 45f96176a6 Initial commit: GUARDiA project setup
- CLAUDE.md: project context and architecture spec
- docs/: system specs, DB schema, messenger integration, deployment engine
- skills/: guardia-deploy, guardia-agent, guardia-messenger
- .claude/settings.json: project-level permissions
- .gitignore: Python/FastAPI project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:50:19 +09:00

2.8 KiB

name description
guardia-deploy 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() 호출 필수

환경 변수

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 명령 차단
  • 자격증명(비밀번호) 로그 출력 금지
  • 헬스체크 없이 다음 노드 진행 금지

코드 패턴

SSH 실행

result = executor.execute_command("sh /app/scripts/startup.sh")
if result["exit_code"] != 0:
    raise DeploymentError(f"startup 실패: {result['stderr']}")

파일 전송 + 무결성

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 불일치"

헬스체크

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 트리거

에러 처리 표준

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 동적 자원 판별

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