AWS Lightsail에서 Spring Boot 무중단 배포 구축하기

지쥬·2025년 11월 20일

AWS

목록 보기
4/4
post-thumbnail

문제 상황

기존 배포 방식의 문제점

기존에는 Spring Boot 애플리케이션을 배포할 때 아래와 같은 방식을 사용

# 1. 실행 중인 프로세스 확인
ps -ef | grep java

# 2. 프로세스 강제 종료
kill -9 <PID>

# 3. 새 버전 실행
cd /home/ubuntu/myapp
nohup java -jar application.jar --spring.profiles.active=prod > app.log 2>&1 &

# 4. 로그 확인
tail -f app.log

문제점:

  • 서버 종료 후 새 서버 시작까지 5-10초의 중단 시간 발생
  • 사용자가 페이지 접속 시 502 Bad Gateway 에러 발생
  • 배포할 때마다 서비스 중단 불가피

무중단 배포(Zero-Downtime Deployment)는 서비스를 중단하지 않고 새로운 버전을 배포하는 방식

해결 방법

  1. 임시 포트로 새 버전 시작: 기존 서버가 실행 중일 때, 다른 포트로 새 버전을 먼저 띄움
  2. Health Check: 새 버전이 완전히 준비될 때까지 대기
  3. 포트 전환: 기존 서버 종료 후 새 서버를 원래 포트로 재시작
  4. Nginx 프록시: 항상 같은 포트만 바라보도록 설정

구현 과정

1. Nginx 설정 파일 생성

로컬 PC에서 nginx_config 파일 생성:

upstream app_backend {
    server 127.0.0.1:8080;
}

server { 
    listen 80; 
    server_name example.com www.example.com; 
    rewrite ^ https://$server_name$request_uri? permanent;
}

server { 
    listen 443 ssl; 
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    
    location /api/stream {
        proxy_pass http://app_backend;
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header Host $http_host;

        proxy_buffering off;
        proxy_request_buffering off;
        proxy_cache off;
        gzip off;
        add_header X-Accel-Buffering no always; 
        proxy_read_timeout 3600s; 
        proxy_send_timeout 3600s;

        proxy_http_version 1.1;
        chunked_transfer_encoding on;
    }
    
    location / { 
        proxy_pass http://app_backend;
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header Host $http_host;
    }
}
  • upstream에서 8080 포트만 지정
  • Nginx는 항상 같은 포트만 바라보므로 안정적

Nginx 설정 적용

# FileZilla로 nginx_config 파일을 /home/ubuntu/myapp/에 업로드 후

# SSH에서 설정 적용
sudo cp /home/ubuntu/myapp/nginx_config /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx

2. 무중단 배포 스크립트 작성

로컬 PC에서 deploy.sh 파일 생성:

#!/bin/bash

echo "======================================"
echo "    Zero-Downtime Deployment"
echo "======================================"
echo ""

# Always use port 8080 for final deployment
FINAL_PORT=8080
TEMP_PORT=8081

# Check if port 8080 is running
OLD_PID=$(lsof -ti:${FINAL_PORT})

if [ -n "$OLD_PID" ]; then
    echo "[INFO] Port ${FINAL_PORT} is running (PID: $OLD_PID)"
    echo "[INFO] Starting new version on temporary port ${TEMP_PORT}..."
else
    echo "[INFO] No running process found"
    echo "[INFO] Starting directly on port ${FINAL_PORT}..."
    TEMP_PORT=${FINAL_PORT}
fi

# Move to working directory
cd /home/ubuntu/myapp

# Start new version on temp port
nohup java -jar application.jar \
    --spring.profiles.active=prod \
    --server.port=${TEMP_PORT} \
    > app_temp.log 2>&1 &

NEW_PID=$!
echo "[INFO] New process PID: $NEW_PID"

# Health check
echo "[INFO] Health checking new application..."
RETRY_COUNT=0
MAX_RETRY=30

while [ $RETRY_COUNT -lt $MAX_RETRY ]; do
    sleep 2
    RETRY_COUNT=$((RETRY_COUNT + 1))
    
    # Check if process is alive
    if ! ps -p $NEW_PID > /dev/null 2>&1; then
        echo "[ERROR] New process failed to start!"
        echo "[ERROR] Check logs: tail -f app_temp.log"
        exit 1
    fi
    
    # Check HTTP response
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${TEMP_PORT}/ 2>/dev/null)
    
    # Success if 2xx response
    if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then
        echo "[SUCCESS] New application started successfully! (HTTP ${HTTP_STATUS})"
        break
    fi
    
    echo "  Waiting... (${RETRY_COUNT}/${MAX_RETRY}) - HTTP Status: ${HTTP_STATUS}"
done

# Timeout check
if [ $RETRY_COUNT -eq $MAX_RETRY ]; then
    echo "[ERROR] New application is not responding!"
    echo "[ERROR] Check logs: tail -f app_temp.log"
    echo "[ERROR] Old process will remain running."
    kill -9 $NEW_PID
    exit 1
fi

# If we used temp port, need to restart on final port
if [ "$TEMP_PORT" = "$FINAL_PORT" ]; then
    echo "[INFO] Already running on port ${FINAL_PORT}"
    mv app_temp.log app_${FINAL_PORT}.log 2>/dev/null
else
    echo "[INFO] Restarting on port ${FINAL_PORT}..."
    
    # Terminate old process first
    if [ -n "$OLD_PID" ]; then
        echo "[INFO] Terminating old process (PID: $OLD_PID)..."
        kill -15 $OLD_PID
        
        # Wait for port to be free
        WAIT_COUNT=0
        while [ $WAIT_COUNT -lt 10 ]; do
            if ! lsof -ti:${FINAL_PORT} > /dev/null 2>&1; then
                echo "[SUCCESS] Port ${FINAL_PORT} is now free"
                break
            fi
            sleep 1
            WAIT_COUNT=$((WAIT_COUNT + 1))
        done
        
        # Force kill if needed
        if lsof -ti:${FINAL_PORT} > /dev/null 2>&1; then
            echo "[WARN] Forcing termination..."
            kill -9 $OLD_PID
            sleep 2
        fi
    fi
    
    # Stop temp process
    echo "[INFO] Stopping temporary process..."
    kill -15 $NEW_PID
    sleep 2
    
    # Start on final port
    echo "[INFO] Starting on port ${FINAL_PORT}..."
    nohup java -jar application.jar \
        --spring.profiles.active=prod \
        --server.port=${FINAL_PORT} \
        > app_${FINAL_PORT}.log 2>&1 &
    
    FINAL_PID=$!
    
    # Quick health check
    echo "[INFO] Final health check..."
    sleep 5
    
    if ps -p $FINAL_PID > /dev/null 2>&1; then
        echo "[SUCCESS] Application running on port ${FINAL_PORT}"
        NEW_PID=$FINAL_PID
    else
        echo "[ERROR] Failed to start on port ${FINAL_PORT}"
        echo "[ERROR] Check logs: tail -f app_${FINAL_PORT}.log"
        exit 1
    fi
fi

echo ""
echo "[SUCCESS] Zero-downtime deployment completed!"
echo ""
echo "Runtime Info:"
echo "  - PID: $NEW_PID"
echo "  - PORT: ${FINAL_PORT}"
echo "  - Log file: /home/ubuntu/myapp/app_${FINAL_PORT}.log"
echo ""
echo "View logs: tail -f app_${FINAL_PORT}.log"
echo "======================================"

스크립트 동작 원리

1단계: 임시 포트(8081)로 새 버전 시작
   ↓ (기존 8080 포트는 계속 서비스 중)
   
2단계: Health Check (최대 60초)
   ↓ (HTTP 200 응답까지 대기)
   
3단계: 기존 서버(8080) 종료
   ↓ (포트 완전히 해제될 때까지 대기)
   
4단계: 새 버전을 8080 포트로 재시작
   ↓
   
완료: 무중단 배포 성공!

스크립트 적용

# FileZilla로 deploy.sh 파일을 /home/ubuntu/myapp/에 업로드 후

# SSH에서 실행 권한 부여
chmod +x /home/ubuntu/myapp/deploy.sh

3. 배포 테스트

실행

cd /home/ubuntu/myapp && ./deploy.sh

실행 결과

======================================
    Zero-Downtime Deployment
======================================

[INFO] Port 8080 is running (PID: 12345)
[INFO] Starting new version on temporary port 8081...
[INFO] New process PID: 12346
[INFO] Health checking new application...
  Waiting... (1/30) - HTTP Status: 000
  Waiting... (2/30) - HTTP Status: 000
  ...
  Waiting... (13/30) - HTTP Status: 000
[SUCCESS] New application started successfully! (HTTP 200)
[INFO] Restarting on port 8080...
[INFO] Terminating old process (PID: 12345)...
[SUCCESS] Port 8080 is now free
[INFO] Stopping temporary process...
[INFO] Starting on port 8080...
[INFO] Final health check...
[SUCCESS] Application running on port 8080

[SUCCESS] Zero-downtime deployment completed!

Runtime Info:
  - PID: 12347
  - PORT: 8080
  - Log file: /home/ubuntu/myapp/app_8080.log

View logs: tail -f app_8080.log
======================================

배포 프로세스 개선

기존 방식 (5단계):

ps -ef | grep java
kill -9 <PID>
cd /home/ubuntu/myapp
nohup java -jar application.jar --spring.profiles.active=prod > app.log 2>&1 &
tail -f app.log

개선 후 (1단계):

cd /home/ubuntu/myapp && ./deploy.sh

트러블슈팅

1. 배포 중 502 에러가 발생하는 경우

원인: 새 애플리케이션이 준비되기 전에 기존 서버가 종료됨

해결: 스크립트의 Health Check이 HTTP 200 응답을 확인한 후에만 전환하므로, 대부분 방지됨. 만약 발생한다면:

# Health Check 대기 시간 늘리기
MAX_RETRY=30MAX_RETRY=50

2. 새 버전 배포 실패 시

증상:

[ERROR] New application is not responding!
[ERROR] Old process will remain running.

결과: 기존 버전이 계속 실행되므로 서비스는 정상

확인:

tail -f app_temp.log  # 에러 로그 확인

3. 포트 충돌 문제

확인:

ps -ef | grep java

해결:

# 불필요한 프로세스 종료
kill -9 <PID>

0개의 댓글