## 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>
276 lines
9.3 KiB
Bash
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 "$@"
|