diff --git a/deployer/app.py b/deployer/app.py index 92dcc60..745bbb7 100644 --- a/deployer/app.py +++ b/deployer/app.py @@ -1,14 +1,18 @@ -import os, hmac, hashlib, subprocess +import os, hmac, hashlib, subprocess, threading from fastapi import FastAPI, Request, HTTPException, BackgroundTasks import logging -# 로깅 설정 -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s") logger = logging.getLogger("deployer") app = FastAPI() SECRET = os.getenv("WEBHOOK_SECRET", "") +if not SECRET: + logger.warning("WEBHOOK_SECRET is not set! All webhooks will be rejected.") + +_deploy_lock = threading.Lock() + def verify(sig: str, body: bytes) -> bool: if not SECRET or not sig: return False @@ -18,19 +22,26 @@ def verify(sig: str, body: bytes) -> bool: return any(hmac.compare_digest(sig, c) for c in candidates) def run_deploy_script(): - """배포 스크립트를 백그라운드에서 실행하고 로그를 남김""" - logger.info("Starting deployment script...") + """배포 스크립트를 백그라운드에서 실행 (동시 실행 방지)""" + if not _deploy_lock.acquire(blocking=False): + logger.info("Deploy already in progress, skipping") + return + try: - # 타임아웃 10분 설정 + logger.info("Starting deployment script...") p = subprocess.run(["/bin/bash", "/scripts/deploy.sh"], capture_output=True, text=True, timeout=600) - + if p.returncode == 0: logger.info(f"Deployment SUCCESS:\n{p.stdout}") else: logger.error(f"Deployment FAILED ({p.returncode}):\n{p.stdout}\n{p.stderr}") - + + except subprocess.TimeoutExpired: + logger.error("Deployment TIMEOUT (10 min exceeded)") except Exception as e: logger.exception(f"Exception during deployment: {e}") + finally: + _deploy_lock.release() @app.get("/health") def health(): diff --git a/docker-compose.yml b/docker-compose.yml index 2bc754b..a9bc69f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,9 @@ services: - WINDOWS_AI_SERVER_URL=${WINDOWS_AI_SERVER_URL:-http://192.168.0.5:8000} - GEMINI_API_KEY=${GEMINI_API_KEY:-} - GEMINI_MODEL=${GEMINI_MODEL:-gemini-1.5-flash} + - ADMIN_API_KEY=${ADMIN_API_KEY:-} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - CORS_ALLOW_ORIGINS=${CORS_ALLOW_ORIGINS:-http://localhost:3007,http://localhost:8080} volumes: - ${STOCK_DATA_PATH:-./data/stock}:/app/data @@ -45,6 +48,7 @@ services: - TZ=${TZ:-Asia/Seoul} - MUSIC_AI_SERVER_URL=${MUSIC_AI_SERVER_URL:-} - MUSIC_MEDIA_BASE=${MUSIC_MEDIA_BASE:-/media/music} + - CORS_ALLOW_ORIGINS=${CORS_ALLOW_ORIGINS:-http://localhost:3007,http://localhost:8080} volumes: - ${MUSIC_DATA_PATH:-./data/music}:/app/data @@ -88,7 +92,7 @@ services: container_name: webpage-deployer restart: unless-stopped ports: - - "19010:9000" # 외부 노출 필요 없으면 내부만 (리버스프록시로 /webhook만 노출 추천) + - "127.0.0.1:19010:9000" # localhost만 허용 (nginx /webhook 프록시 경유) environment: - WEBHOOK_SECRET=${WEBHOOK_SECRET} volumes: diff --git a/scripts/deploy-nas.sh b/scripts/deploy-nas.sh index 6d45015..595c2a9 100644 --- a/scripts/deploy-nas.sh +++ b/scripts/deploy-nas.sh @@ -30,33 +30,14 @@ cd "$SRC" # 레포에서 운영으로 반영할 항목들만 복사/동기화 (필요한 것만 적기) # backend, travel-proxy, deployer, nginx, scripts, docker-compose.yml, .env 등 -rsync -a --delete \ - --exclude ".git" \ - --exclude ".releases" \ - "$SRC/backend/" "$DST/backend/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/travel-proxy/" "$DST/travel-proxy/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/deployer/" "$DST/deployer/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/stock-lab/" "$DST/stock-lab/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/music-lab/" "$DST/music-lab/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/nginx/" "$DST/nginx/" -rsync -a --delete \ - --exclude ".git" \ - "$SRC/scripts/" "$DST/scripts/" +RSYNC_EXCLUDES="--exclude .git --exclude __pycache__ --exclude *.pyc --exclude data/" -# compose 파일 / env 파일 +for dir in backend travel-proxy deployer stock-lab music-lab nginx scripts; do + rsync -a --delete $RSYNC_EXCLUDES \ + "$SRC/$dir/" "$DST/$dir/" +done + +# compose 파일만 동기화 (.env는 절대 동기화하지 않음 — 운영 시크릿 보호) rsync -a "$SRC/docker-compose.yml" "$DST/docker-compose.yml" -if [ -f "$SRC/.env" ]; then - rsync -a "$SRC/.env" "$DST/.env" -fi echo "SYNC_OK" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 6db2f2c..a12ffea 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,6 +1,10 @@ #!/bin/bash set -euo pipefail +# ── 동시 배포 방지 (flock) ── +exec 200>/tmp/deploy.lock +flock -n 200 || { echo "Deploy already running, skipping"; exit 0; } + # 1. 자동 감지: Docker 컨테이너 내부인가? if [ -d "/repo" ] && [ -d "/runtime" ]; then echo "Detected Docker Container environment." @@ -13,10 +17,9 @@ else set -a; source .env; set +a fi - # 환경변수가 없으면 현재 디렉토리를 SRC로 SRC="${REPO_PATH:-$(pwd)}" - DST="${RUNTIME_PATH:-/volume1/docker/webpage}" # 기본값 설정 - + DST="${RUNTIME_PATH:-/volume1/docker/webpage}" + if [ -z "$DST" ]; then echo "Error: RUNTIME_PATH is not set." exit 1 @@ -32,19 +35,40 @@ cd "$SRC" git fetch --all --prune git pull --ff-only -# 릴리즈 백업(롤백용): 아래 5번과 연결 +# ── 릴리즈 백업 (롤백용) ── TAG="$(date +%Y%m%d-%H%M%S)" -mkdir -p "$DST/.releases/$TAG" +BACKUP_DIR="$DST/.releases/$TAG" +mkdir -p "$BACKUP_DIR.tmp" rsync -a --delete \ --exclude ".releases" \ - "$DST/" "$DST/.releases/$TAG/" + "$DST/" "$BACKUP_DIR.tmp/" +mv "$BACKUP_DIR.tmp" "$BACKUP_DIR" -# 소스 → 운영 반영 (네가 이미 만든 deploy-nas.sh가 있으면 그걸 호출해도 됨) -# 예: repo/scripts/deploy-nas.sh가 운영으로 복사/동기화하는 로직이라면: +# 오래된 릴리즈 정리 (최근 5개만 유지) +ls -dt "$DST/.releases"/*/ 2>/dev/null | tail -n +6 | xargs -r rm -rf + +# ── 소스 → 운영 반영 ── bash "$SRC/scripts/deploy-nas.sh" +# ── 컨테이너 재빌드 (deployer 제외 — 자기 자신을 재빌드하면 스크립트 중단됨) ── cd "$DST" -docker compose up -d --build backend travel-proxy stock-lab frontend deployer +docker compose up -d --build backend travel-proxy stock-lab music-lab frontend docker exec lotto-frontend nginx -s reload 2>/dev/null || true -echo "DEPLOY_OK $TAG" +# ── 배포 후 헬스체크 ── +echo "Waiting for services to start..." +sleep 5 + +HEALTH_OK=true +for endpoint in "http://backend:8000/health" "http://stock-lab:8000/health" "http://travel-proxy:8000/health" "http://music-lab:8000/health"; do + if ! curl -sf --max-time 5 "$endpoint" > /dev/null 2>&1; then + echo "HEALTH_FAIL: $endpoint" + HEALTH_OK=false + fi +done + +if [ "$HEALTH_OK" = false ]; then + echo "DEPLOY_WARN: Some services failed health check. Consider rollback: $TAG" +else + echo "DEPLOY_OK $TAG" +fi