- 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>
94 lines
2.8 KiB
Markdown
94 lines
2.8 KiB
Markdown
---
|
|
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 명령 차단
|
|
- 자격증명(비밀번호) 로그 출력 금지
|
|
- 헬스체크 없이 다음 노드 진행 금지
|
|
|
|
## 코드 패턴
|
|
|
|
### 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
|
|
```
|