#!/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 "$@"