signal-batch/scripts/deploy.sh
htlee dd694bdcbb feat: React 19 SPA Dashboard Phase 1 + 안전 배포 시스템
## React SPA Dashboard
- React 19 + Vite 7 + Tailwind CSS 4 + Recharts 2 SPA 구축
- Dashboard (배치현황/시스템메트릭/캐시/처리량) + JobMonitor (이력조회/Step상세)
- i18n 다국어(ko/en) 시스템, Light/Dark 테마 CSS 토큰 전환
- frontend-maven-plugin 1.15.1 (mvn package 시 자동 빌드)
- WebViewController SPA forward + context-path /signal-batch
- 레거시 HTML 48개 파일 전체 삭제

## 안전 배포
- VesselBatchScheduler @PreDestroy: 신규 Job 차단 + 실행 중 Job 완료 대기
- server.shutdown=graceful, timeout-per-shutdown-phase=3m
- deploy.yml: 활성 Job 3초 연속 확인 후 stop → 교체 → start
- signal-batch.service TimeoutStopSec 60→180
- scripts/deploy.sh: 수동 배포용 안전 스크립트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:05:38 +09:00

276 lines
9.3 KiB
Bash

#!/bin/bash
# =============================================================================
# Safe Deploy Script — API 기반 활성 Job 확인 후 무중단 배포
#
# 동작:
# 1. /admin/batch/job/running API를 1초 간격으로 폴링
# 2. 활성 Job 없음 → 카운트 증가, 활성 Job 발생 → 카운트 초기화
# 3. 연속 3회 확인 (SAFE_COUNT_THRESHOLD) → SIGTERM → 프로세스 종료 대기 → 재기동
#
# 사용법:
# ./deploy.sh # 기본 (JAR 교체 없이 재기동)
# ./deploy.sh /path/to/new.jar # 새 JAR 배포 후 기동
# PROFILE=dev ./deploy.sh # 프로파일 지정
# =============================================================================
set -euo pipefail
# ── 설정 ────────────────────────────────────────────────────────────────────
APP_HOME="${APP_HOME:-/home/apps/signal-batch}"
JAR_FILE="$APP_HOME/vessel-batch-aggregation.jar"
PID_FILE="$APP_HOME/vessel-batch.pid"
LOG_DIR="$APP_HOME/logs"
PROFILE="${PROFILE:-prod}"
PORT="${PORT:-18090}"
CONTEXT_PATH="${CONTEXT_PATH:-/signal-batch}"
BASE_URL="http://localhost:${PORT}${CONTEXT_PATH}"
JAVA_HOME="${JAVA_HOME:-/devdata/apps/jdk-17.0.8}"
JAVA_BIN="$JAVA_HOME/bin/java"
# 폴링 설정
POLL_INTERVAL=1 # 1초 간격
SAFE_COUNT_THRESHOLD=3 # 3회 연속 활성 Job 없음 확인
MAX_WAIT_FOR_IDLE=300 # 최대 5분 대기 (활성 Job 종료 대기)
MAX_WAIT_FOR_STOP=60 # 프로세스 종료 최대 대기 (초)
STARTUP_TIMEOUT=60 # 기동 확인 타임아웃 (초)
# 색상
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ── 유틸리티 ────────────────────────────────────────────────────────────────
timestamp() { date '+%Y-%m-%d %H:%M:%S'; }
log_info() { echo -e "${CYAN}[$(timestamp)]${NC} $*"; }
log_ok() { echo -e "${GREEN}[$(timestamp)] ✓${NC} $*"; }
log_warn() { echo -e "${YELLOW}[$(timestamp)] ⚠${NC} $*"; }
log_error() { echo -e "${RED}[$(timestamp)] ✗${NC} $*"; }
get_pid() {
if [ -f "$PID_FILE" ]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
return
fi
rm -f "$PID_FILE"
fi
pgrep -f "$JAR_FILE" 2>/dev/null || true
}
# ── 1단계: 활성 Job 없음 확인 (3초 연속) ────────────────────────────────────
wait_for_idle() {
local safe_count=0
local elapsed=0
log_info "활성 Job 확인 시작 (${SAFE_COUNT_THRESHOLD}회 연속 확인 필요)"
while [ $elapsed -lt $MAX_WAIT_FOR_IDLE ]; do
local response
local http_code
http_code=$(curl -s -o /tmp/deploy_running_jobs.json -w '%{http_code}' \
"${BASE_URL}/admin/batch/job/running" 2>/dev/null || echo "000")
if [ "$http_code" = "000" ]; then
log_warn "API 응답 없음 (앱 미실행 또는 네트워크 문제) — 바로 배포 진행"
return 0
fi
if [ "$http_code" != "200" ]; then
log_warn "API 응답 코드: $http_code — 재시도"
safe_count=0
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
continue
fi
# JSON 배열 길이 확인 (jq 없으면 grep 폴백)
local running_count
if command -v jq >/dev/null 2>&1; then
running_count=$(jq 'length' /tmp/deploy_running_jobs.json 2>/dev/null || echo "-1")
else
# jq 없을 때: 빈 배열 "[]"이면 0, 아니면 1 이상
if grep -q '^\[\]$' /tmp/deploy_running_jobs.json 2>/dev/null; then
running_count=0
else
running_count=1
fi
fi
if [ "$running_count" = "0" ]; then
safe_count=$((safe_count + 1))
log_info "활성 Job 없음 (${safe_count}/${SAFE_COUNT_THRESHOLD})"
if [ $safe_count -ge $SAFE_COUNT_THRESHOLD ]; then
log_ok "연속 ${SAFE_COUNT_THRESHOLD}회 확인 완료 — 안전 배포 가능"
rm -f /tmp/deploy_running_jobs.json
return 0
fi
else
if [ $safe_count -gt 0 ]; then
log_warn "활성 Job 감지 — 카운트 초기화 (running: ${running_count}건)"
fi
safe_count=0
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
done
rm -f /tmp/deploy_running_jobs.json
log_error "최대 대기시간 ${MAX_WAIT_FOR_IDLE}초 초과 — 활성 Job이 계속 존재"
return 1
}
# ── 2단계: 프로세스 종료 ─────────────────────────────────────────────────────
stop_process() {
local pid
pid=$(get_pid)
if [ -z "$pid" ]; then
log_info "실행 중인 프로세스 없음"
return 0
fi
log_info "프로세스 종료 요청 (PID: $pid, SIGTERM)"
kill -15 "$pid"
local waited=0
while [ $waited -lt $MAX_WAIT_FOR_STOP ]; do
if ! kill -0 "$pid" 2>/dev/null; then
log_ok "프로세스 정상 종료 (${waited}초)"
rm -f "$PID_FILE"
return 0
fi
sleep 1
waited=$((waited + 1))
done
log_warn "정상 종료 타임아웃 (${MAX_WAIT_FOR_STOP}초) — SIGKILL"
kill -9 "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
sleep 1
}
# ── 3단계: JAR 교체 (선택) ───────────────────────────────────────────────────
deploy_jar() {
local new_jar="$1"
if [ -z "$new_jar" ]; then
log_info "JAR 교체 없음 — 기존 JAR로 재기동"
return 0
fi
if [ ! -f "$new_jar" ]; then
log_error "새 JAR 파일 없음: $new_jar"
return 1
fi
log_info "JAR 교체: $new_jar$JAR_FILE"
# 백업
if [ -f "$JAR_FILE" ]; then
cp "$JAR_FILE" "${JAR_FILE}.bak"
log_info "기존 JAR 백업: ${JAR_FILE}.bak"
fi
cp "$new_jar" "$JAR_FILE"
log_ok "JAR 교체 완료"
}
# ── 4단계: 프로세스 기동 ─────────────────────────────────────────────────────
start_process() {
mkdir -p "$LOG_DIR"
# JVM 옵션
local jvm_heap=8
if command -v free >/dev/null 2>&1; then
local total_mem
total_mem=$(free -g | grep Mem | awk '{print $2}')
jvm_heap=$((total_mem / 8))
[ $jvm_heap -lt 8 ] && jvm_heap=8
[ $jvm_heap -gt 16 ] && jvm_heap=16
fi
local java_opts="-Xms${jvm_heap}g -Xmx${jvm_heap}g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=$LOG_DIR/heapdump.hprof \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Seoul \
-Dspring.profiles.active=$PROFILE"
log_info "프로세스 기동 (profile=$PROFILE, heap=${jvm_heap}GB)"
cd "$APP_HOME"
nohup "$JAVA_BIN" $java_opts -jar "$JAR_FILE" \
> "$LOG_DIR/app.log" 2>&1 &
local new_pid=$!
echo "$new_pid" > "$PID_FILE"
log_info "PID: $new_pid"
# 기동 확인
local waited=0
while [ $waited -lt $STARTUP_TIMEOUT ]; do
if grep -q "Started SignalBatchApplication" "$LOG_DIR/app.log" 2>/dev/null; then
log_ok "애플리케이션 기동 완료 (${waited}초)"
return 0
fi
if ! kill -0 "$new_pid" 2>/dev/null; then
log_error "프로세스가 기동 중 종료됨"
tail -20 "$LOG_DIR/app.log"
return 1
fi
sleep 1
waited=$((waited + 1))
done
log_warn "기동 확인 타임아웃 (${STARTUP_TIMEOUT}초) — 로그 확인 필요"
tail -10 "$LOG_DIR/app.log"
}
# ── 메인 ─────────────────────────────────────────────────────────────────────
main() {
local new_jar="${1:-}"
echo "========================================"
echo " Safe Deploy — Signal Batch"
echo " $(timestamp)"
echo " Profile: $PROFILE | Port: $PORT"
echo "========================================"
echo ""
# 1. 활성 Job 없음 확인
if ! wait_for_idle; then
log_error "배포 중단 — 수동 확인 필요"
exit 1
fi
# 2. 프로세스 종료
stop_process
# 3. JAR 교체 (인자가 있을 때만)
deploy_jar "$new_jar"
# 4. 기동
start_process
echo ""
echo "========================================"
log_ok "배포 완료"
echo " PID: $(cat "$PID_FILE" 2>/dev/null || echo 'N/A')"
echo " Log: tail -f $LOG_DIR/app.log"
echo " Health: curl ${BASE_URL}/actuator/health"
echo "========================================"
}
main "$@"