🛠️ 개발 도구 & 환경

🔄 Nginx를 활용한 Graceful Shutdown 무중단 배포 (Zero Downtime)

twoweekhee 2025. 6. 13. 15:30

서비스 운영 중 배포는 항상 조심스러운 작업입니다. 사용자에게 서비스 중단 없이 새로운 버전을 배포하는 것은 모든 개발팀의 목표이죠. 오늘은 Nginx의 upstream 기능을 활용해서 무중단 배포를 구현하는 방법을 알아보겠습니다! 💪

 

📋 목차

  1. Graceful Deployment란?
  2. Nginx Upstream 설정
  3. 배포 스크립트 구현
  4. Keep-Alive 고려사항
  5. 실제 운영 시나리오
  6. 트러블슈팅
  7. 마무리

🤔 Graceful Deployment란?

Graceful Deployment는 서비스 중단 없이 새로운 버전을 배포하는 방식입니다. 기존 요청들은 정상적으로 처리를 완료하고, 새로운 요청들은 업데이트된 서버로 라우팅되도록 하는 것이 핵심이에요.

기본 아이디어 💡

  1. 서버를 down 상태로 변경 - 새로운 트래픽이 해당 서버로 가지 않도록 설정
  2. 기존 연결 완료 대기 - Keep-Alive 시간 + 여유시간만큼 대기
  3. 안전하게 배포 진행 - 모든 요청이 완료된 후 배포 수행

⚙️ Nginx Upstream 설정

먼저 기본적인 Nginx 설정부터 살펴보겠습니다.

nginx.conf 기본 설정

upstream app_servers {
    server server1:8080;
    server server2:8080;
    server server3:8080;
    
    # 헬스체크 설정
    keepalive 32;
}

server {
    listen 80;
    server_name ${SERVER_NAME};

    # Keep-Alive 설정
    keepalive_timeout 65s;
    keepalive_requests 1000;

    location / {
        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 타임아웃 설정
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # 헬스체크 엔드포인트
    location /health {
        access_log off;
        add_header Content-Type text/plain;
        return 200 "healthy\n";
    }
}

🔧 배포 스크립트 구현

이제 실제 배포를 수행하는 스크립트를 작성해보겠습니다.

deploy.sh - 메인 배포 스크립트

#!/bin/bash

set -e

# 환경변수 설정
NGINX_CONF_PATH="/etc/nginx/nginx.conf"
KEEP_ALIVE_TIME=${KEEP_ALIVE_TIME:-65}
GRACE_PERIOD=${GRACE_PERIOD:-10}
TOTAL_WAIT_TIME=$((KEEP_ALIVE_TIME + GRACE_PERIOD))

# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 서버 down 설정 함수
mark_server_down() {
    local server_name=$1
    log_info "Marking server ${server_name} as down..."
    
    sed -i "s/server ${server_name}:[0-9]*;/server ${server_name}:8080 down;/" $NGINX_CONF_PATH
    
    # 설정 검증
    if nginx -t; then
        nginx -s reload
        log_info "Server ${server_name} marked as down successfully"
    else
        log_error "Nginx configuration test failed"
        exit 1
    fi
}

# 서버 up 설정 함수
mark_server_up() {
    local server_name=$1
    log_info "Marking server ${server_name} as up..."
    
    sed -i "s/server ${server_name}:[0-9]* down;/server ${server_name}:8080;/" $NGINX_CONF_PATH
    
    if nginx -t; then
        nginx -s reload
        log_info "Server ${server_name} marked as up successfully"
    else
        log_error "Nginx configuration test failed"
        exit 1
    fi
}

# 연결 대기 함수
wait_for_connections() {
    log_info "Waiting ${TOTAL_WAIT_TIME}s for existing connections to complete..."
    
    for i in $(seq 1 $TOTAL_WAIT_TIME); do
        echo -n "."
        sleep 1
    done
    echo ""
    log_info "Wait completed"
}

# 애플리케이션 배포 함수
deploy_application() {
    local server_name=$1
    log_info "Deploying application to ${server_name}..."
    
    # 실제 배포 로직을 여기에 구현
    # 예: Docker 컨테이너 재시작, 코드 업데이트 등
    
    # 예제: Docker 컨테이너 재시작
    docker-compose -f /path/to/docker-compose.yml pull app
    docker-compose -f /path/to/docker-compose.yml up -d app
    
    # 헬스체크 대기
    wait_for_health_check $server_name
    
    log_info "Application deployed successfully to ${server_name}"
}

# 헬스체크 함수
wait_for_health_check() {
    local server_name=$1
    local max_attempts=30
    local attempt=1
    
    log_info "Waiting for ${server_name} health check..."
    
    while [ $attempt -le $max_attempts ]; do
        if curl -f "http://${server_name}:8080/health" >/dev/null 2>&1; then
            log_info "Health check passed for ${server_name}"
            return 0
        fi
        
        log_warn "Health check attempt ${attempt}/${max_attempts} failed, retrying..."
        sleep 2
        attempt=$((attempt + 1))
    done
    
    log_error "Health check failed for ${server_name} after ${max_attempts} attempts"
    exit 1
}

# 메인 배포 로직
main() {
    local servers=("server1" "server2" "server3")
    
    log_info "Starting graceful deployment process..."
    
    for server in "${servers[@]}"; do
        log_info "🚀 Processing server: ${server}"
        
        # 1. 서버를 down으로 마킹
        mark_server_down $server
        
        # 2. 기존 연결 완료 대기
        wait_for_connections
        
        # 3. 애플리케이션 배포
        deploy_application $server
        
        # 4. 서버를 다시 up으로 마킹
        mark_server_up $server
        
        log_info "✅ Server ${server} deployment completed"
        echo "----------------------------------------"
    done
    
    log_info "🎉 All servers deployed successfully!"
}

# 스크립트 실행
main "$@"

health-check.sh - 헬스체크 스크립트

#!/bin/bash

SERVER_URL=${1:-"http://localhost:8080"}
MAX_ATTEMPTS=${2:-10}
SLEEP_INTERVAL=${3:-2}

for i in $(seq 1 $MAX_ATTEMPTS); do
    if curl -f "${SERVER_URL}/health" >/dev/null 2>&1; then
        echo "✅ Health check passed"
        exit 0
    fi
    
    echo "❌ Health check failed (attempt ${i}/${MAX_ATTEMPTS})"
    sleep $SLEEP_INTERVAL
done

echo "🚨 Health check failed after ${MAX_ATTEMPTS} attempts"
exit 1

⏰ Keep-Alive 고려사항

Keep-Alive 설정은 graceful deployment의 핵심입니다. 클라이언트와 서버 간의 연결 지속 시간을 고려해야 해요.

주요 설정값들

# 클라이언트와의 Keep-Alive
keepalive_timeout 65s;        # 클라이언트 연결 유지 시간
keepalive_requests 1000;      # 연결당 최대 요청 수

# 업스트림과의 Keep-Alive  
upstream app_servers {
    server server1:8080;
    server server2:8080;
    server server3:8080;
    keepalive 32;             # 업스트림 연결 풀 크기
}

대기 시간 계산 📊

총 대기 시간 = Keep-Alive Timeout + Grace Period
             = 65초 + 10초 = 75초

이렇게 여유 시간을 두는 이유는:

  • 네트워크 지연으로 인한 요청 처리 시간
  • 애플리케이션 내부 처리 시간
  • 예상치 못한 상황에 대한 버퍼

🎯 실제 운영 시나리오

Docker Compose를 사용한 배포

# docker-compose.yml
version: '3.8'

services:
  app:
    image: ${APP_IMAGE:-myapp:latest}
    ports:
      - "8080:8080"
    environment:
      - NODE_ENV=${NODE_ENV:-production}
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

배포 자동화 예제

#!/bin/bash
# auto-deploy.sh

set -e

# 환경 변수 로드
source .env

# 새 이미지 빌드
docker build -t myapp:${BUILD_NUMBER} .
docker tag myapp:${BUILD_NUMBER} myapp:latest

# 배포 실행
./deploy.sh

# 이전 이미지 정리 (선택사항)
docker image prune -f

🔍 트러블슈팅

자주 발생하는 문제들

1. 헬스체크 실패 🚨

# 디버깅용 헬스체크
curl -v http://server1:8080/health

# 로그 확인
docker logs container_name

# 포트 확인
netstat -tlnp | grep 8080

2. Nginx 설정 오류 ⚠️

# 설정 파일 검증
nginx -t

# 설정 다시 로드
nginx -s reload

# 프로세스 확인
ps aux | grep nginx

3. 세션 유지 문제 🔄

만약 세션 기반 애플리케이션이라면:

upstream app_servers {
    ip_hash;  # 클라이언트 IP 기반 세션 유지
    server server1:8080;
    server server2:8080;
    server server3:8080;
}

모니터링 및 로깅 📊

# 실시간 연결 상태 확인
watch -n 1 "netstat -an | grep :8080 | wc -l"

# Nginx 액세스 로그 모니터링
tail -f /var/log/nginx/access.log | grep -E "(200|500|502|503)"

# 시스템 리소스 확인
htop

🎁 추가 개선사항

1. 롤백 기능

# rollback.sh
#!/bin/bash

PREVIOUS_IMAGE=${1:-"myapp:previous"}

log_info "Rolling back to ${PREVIOUS_IMAGE}..."

for server in "${servers[@]}"; do
    mark_server_down $server
    
    # 이전 버전으로 롤백
    docker tag $PREVIOUS_IMAGE myapp:latest
    docker-compose up -d app
    
    wait_for_health_check $server
    mark_server_up $server
done

2. 배포 시간 측정 ⏱️

# 배포 시작 시간 기록
DEPLOY_START_TIME=$(date +%s)

# ... 배포 로직 ...

# 배포 완료 시간 계산
DEPLOY_END_TIME=$(date +%s)
DEPLOY_DURATION=$((DEPLOY_END_TIME - DEPLOY_START_TIME))

log_info "Total deployment time: ${DEPLOY_DURATION} seconds"

🏁 마무리

Nginx를 활용한 graceful deployment는 서비스의 안정성을 크게 향상시킬 수 있는 중요한 기법입니다. 핵심은 다음과 같아요:

  1. 점진적 트래픽 차단 - down 설정으로 새 요청 차단
  2. 충분한 대기 시간 - Keep-Alive + 여유 시간 확보
  3. 헬스체크 검증 - 배포 후 서비스 정상 동작 확인
  4. 모니터링 및 로깅 - 배포 과정 추적 및 문제 해결

이런 방식으로 구현하면 사용자는 서비스 중단을 전혀 느끼지 못하면서도 안전하게 새로운 버전을 배포할 수 있습니다! 🎉

더 궁금한 점이 있으시면 언제든 댓글로 남겨주세요. 함께 더 나은 배포 전략을 만들어가요! 💪

 

하지만 돈 짱 많아서  Nginx plus 쓰고 시푸다..


🏷️ Tags

#nginx #graceful-deployment #무중단배포 #devops #docker #load-balancer #upstream #keep-alive #health-check #automation #deployment-strategy #서버운영 #인프라 #배포자동화 #웹서버