
기존에는 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
문제점:
무중단 배포(Zero-Downtime Deployment)는 서비스를 중단하지 않고 새로운 버전을 배포하는 방식
로컬 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 포트만 지정# 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
로컬 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
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
원인: 새 애플리케이션이 준비되기 전에 기존 서버가 종료됨
해결: 스크립트의 Health Check이 HTTP 200 응답을 확인한 후에만 전환하므로, 대부분 방지됨. 만약 발생한다면:
# Health Check 대기 시간 늘리기
MAX_RETRY=30 → MAX_RETRY=50
증상:
[ERROR] New application is not responding!
[ERROR] Old process will remain running.
결과: 기존 버전이 계속 실행되므로 서비스는 정상
확인:
tail -f app_temp.log # 에러 로그 확인
확인:
ps -ef | grep java
해결:
# 불필요한 프로세스 종료
kill -9 <PID>