∙Server

Nginx와 헬스체크를 활용한 무중단 배포하기

coor 2024. 9. 2. 17:29
 

 

블루-그린 배포 방식인 Nginx의 로드 밸런싱 기능과 헬스체크를 통해 무중단 배포에 대해 알아보겠습니다.

 

무중단 배포 시나리오는 다음과 같습니다.

  1. 새로운 배포 시 두 인스턴스 중 하나만 업데이트하고 Nginx가 트래픽을 새 인스턴스로 라우팅하도록 합니다.
  2. 배포가 완료되면, 나머지 인스턴스도 동일하게 업데이트합니다.
  3. 이 과정에서 Nginx가 트래픽을 분산하여 중단 없이 배포를 수행할 수 있습니다.

 

 

개발 환경 : AWS EC2(ubuntu 24), Nginx 1.24.0, Spring Boot 3

1. 헬스 체크 API 만들기


헬스 체크는 애플리케이션 인스턴스가 정상적으로 작동하는지 확인하는 메커니즘입니다.
간단한 헬스 체크 API 만듭니다.

@RestController
public class KeepAliveController {
    @GetMapping("/health")
    public String ping() {
        return "ok";
    }
}

 

 

 

2. Nginx 로드 밸런싱 설정


Nginx는 로드 밸런서로서 다수의 애플리케이션 인스턴스에 트래픽을 분산시킬 수 있습니다. 
애플리케이션 인스턴스 구성은 다음과 같습니다. 

  • 첫 번째 애플리케이션 : 127.0.0.1:8080
  • 두 번째 애플리케이션 : 127.0.0.1:8081

 

두 개의 애플리케이션 인스턴스를 사용할 수 있도록 Nginx 설정을 업데이트해야 합니다.

Nginx 설정

  • /etc/nginx/nginx.conf
http {
    upstream backend {
        server localhost:8080;
        server localhost:8081;
    }

    server {
        server_name api.seolyu.com;

        location / {
            proxy_pass http://backend;
            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;
        }
    }
}

인스턴스를 구분하기 위해 upstream을 사용합니다.

 

 

설정을 적용하기 위해 Nginx 재시작합니다.

sudo nginx -t # 설정 테스트 
sudo systemctl restart nginx  # nginx 재시작

 

 

 

 

3. 배포 스크립트 작성


무중단 배포를 위한 배포 스크립트 다음과 같습니다.

REPOSITORY=/home/ubuntu/seolyu
PROJECT_NAME=action

# 첫 번째 인스턴스 포트 (8080)
PORT1=8080

# 두 번째 인스턴스 포트 (8081)
PORT2=8081

log() {
  echo "$1" | tee -a $REPOSITORY/deploy.log
}
  • REPOSITORY: 애플리케이션의 루트 디렉토리 경로
  • PROJECT_NAME: 프로젝트 이름
  • PORT1과 PORT2: 각각의 애플리케이션 인스턴스가 사용할 포트 번호
  • log(): 로그 메시지를 deploy.log 파일에 추가하고 동시에 콘솔에 출력합니다.

 

 

log ">------------------------배포 시작------------------------"
# .jar 파일 복사
log "> Build 파일 복사"
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/

# 새 애플리케이션 배포 준비
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
chmod +x $JAR_NAME
  • 빌드한 .jar 파일을 복사하고 복사한 파일에 실행 권한을 줍니다.

 

# 1. 첫 번째 인스턴스 종료 및 두 번째 인스턴스로 트래픽 이동
log "> 현재 구동중인 첫 번째 애플리케이션(pid 확인)"
CURRENT_PID1=$(pgrep -f ".*-Dserver.port=$PORT1.*.jar")

log "> 현재 구동중인 첫 번째 애플리케이션 pid : $CURRENT_PID1"

if [ -z $CURRENT_PID1 ]
then
  log "> 첫 번째 애플리케이션이 구동 중이 아니므로 종료하지 않습니다."
else
  log "> 첫 번째 애플리케이션 종료 : kill -15 $CURRENT_PID1"
  kill -15 $CURRENT_PID1
  log "> Waiting for 5 seconds..."
  sleep 5
fi

# 2. 첫 번째 인스턴스에 새 버전 배포
log "> 첫 번째 인스턴스에 새 버전 배포 (PORT: $PORT1)"
nohup java -jar \
  -Dserver.port=$PORT1 \
  $JAR_NAME > $REPOSITORY/nohup1.out 2>&1 &
  • 첫 번째 인스턴스가 실행 중인지 확인하고, 실행 중인 경우 종료합니다.
  • 첫 번째 애플리케이션 인스턴스를 8081 포트에 배포합니다.

 

# 3. 첫 번째 인스턴스 헬스 체크
log "> 첫 번째 인스턴스 헬스 체크 시작"

for i in $(seq 1 10)
do
    RESPONSE=$(curl -s http://localhost:$PORT1/health)
    OK_COUNT=$(echo "$RESPONSE" | grep -c 'ok')

    if [ $OK_COUNT -ge 1 ]
    then
        log "> 첫 번째 인스턴스가 성공적으로 시작되었습니다."
        break
    else
        log "> 첫 번째 인스턴스가 아직 시작되지 않았습니다. 10초 후 다시 시도합니다."
        sleep 10
    fi

    if [ $i -eq 10 ]
    then
        log "> 첫 번째 인스턴스가 시작되지 않았습니다. 배포를 중단합니다."
        exit 1
    fi
done
  • 첫 번째 인스턴스의 /health 엔드포인트를 10회 시도하여 애플리케이션이 정상적으로 시작되었는지 확인합니다.
  • 성공 시 배포를 계속 진행하고, 실패 시 배포를 중단합니다.

 

# 4. 두 번째 인스턴스 종료 및 첫 번째 인스턴스로 트래픽 이동
log "> 현재 구동중인 두 번째 애플리케이션(pid 확인)"
CURRENT_PID2=$(pgrep -f ".*-Dserver.port=$PORT2.*.jar")

log "> 현재 구동중인 두 번째 애플리케이션 pid : $CURRENT_PID2"

if [ -z $CURRENT_PID2 ]
then
  log "> 두 번째 애플리케이션이 구동 중이 아니므로 종료하지 않습니다."
else
  log "> 두 번째 애플리케이션 종료 : kill -15 $CURRENT_PID2"
  kill -15 $CURRENT_PID2
  log "> Waiting for 5 seconds..."
  sleep 5
fi

# 5. 두 번째 인스턴스에 새 버전 배포
log "> 두 번째 인스턴스에 새 버전 배포 (PORT: $PORT2)"
nohup java -jar \
  -Dserver.port=$PORT2 \
  $JAR_NAME > $REPOSITORY/nohup2.out 2>&1 &
  • 두 번째 인스턴스가 실행 중인지 확인하고, 실행 중인 경우 종료합니다.
  • 두 번째 애플리케이션 인스턴스를 8082 포트에 배포합니다.

 

 

# 6. 두 번째 인스턴스 헬스 체크
log "> 두 번째 인스턴스 헬스 체크 시작"

for i in $(seq 1 10)
do
    RESPONSE=$(curl -s http://localhost:$PORT2/health)
    OK_COUNT=$(echo "$RESPONSE" | grep -c 'ok')

    if [ $OK_COUNT -ge 1 ]
    then
        log "> 두 번째 인스턴스가 성공적으로 시작되었습니다."
        break
    else
        log "> 두 번째 인스턴스가 아직 시작되지 않았습니다. 10초 후 다시 시도합니다."
        sleep 10
    fi

    if [ $i -eq 10 ]
    then
        log "> 두 번째 인스턴스가 시작되지 않았습니다. 배포를 중단합니다."
        exit 1
    fi
done

log ">------------------------배포 종료------------------------"
  • 두 번째 인스턴스의 /health 엔드포인트를 10회 시도하여 애플리케이션이 정상적으로 시작되었는지 확인합니다.
  • 성공 시 배포를 완료하고, 실패 시 배포를 중단합니다.

 

deploy.sh 전체 코드

더보기
REPOSITORY=/home/ubuntu/seolyu
PROJECT_NAME=action

# 첫 번째 인스턴스 포트 (8080)
PORT1=8080

# 두 번째 인스턴스 포트 (8081)
PORT2=8081

log() {
  echo "$1" | tee -a $REPOSITORY/deploy.log
}

log ">------------------------배포 시작------------------------"
# .jar 파일 복사
log "> Build 파일 복사"
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/

# 새 애플리케이션 배포 준비
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
chmod +x $JAR_NAME

# 1. 첫 번째 인스턴스 종료 및 두 번째 인스턴스로 트래픽 이동
log "> 현재 구동중인 첫 번째 애플리케이션(pid 확인)"
CURRENT_PID1=$(pgrep -f ".*-Dserver.port=$PORT1.*.jar")

log "> 현재 구동중인 첫 번째 애플리케이션 pid : $CURRENT_PID1"

if [ -z $CURRENT_PID1 ]
then
  log "> 첫 번째 애플리케이션이 구동 중이 아니므로 종료하지 않습니다."
else
  log "> 첫 번째 애플리케이션 종료 : kill -15 $CURRENT_PID1"
  kill -15 $CURRENT_PID1
  log "> Waiting for 5 seconds..."
  sleep 5
fi

# 2. 첫 번째 인스턴스에 새 버전 배포
log "> 첫 번째 인스턴스에 새 버전 배포 (PORT: $PORT1)"
nohup java -jar \
  -Dserver.port=$PORT1 \
  $JAR_NAME > $REPOSITORY/nohup1.out 2>&1 &

# 3. 첫 번째 인스턴스 헬스 체크
log "> 첫 번째 인스턴스 헬스 체크 시작"

for i in $(seq 1 10)
do
    RESPONSE=$(curl -s http://localhost:$PORT1/health)
    OK_COUNT=$(echo "$RESPONSE" | grep -c 'ok')

    if [ $OK_COUNT -ge 1 ]
    then
        log "> 첫 번째 인스턴스가 성공적으로 시작되었습니다."
        break
    else
        log "> 첫 번째 인스턴스가 아직 시작되지 않았습니다. 10초 후 다시 시도합니다."
        sleep 10
    fi

    if [ $i -eq 10 ]
    then
        log "> 첫 번째 인스턴스가 시작되지 않았습니다. 배포를 중단합니다."
        exit 1
    fi
done

# 4. 두 번째 인스턴스 종료 및 첫 번째 인스턴스로 트래픽 이동
log "> 현재 구동중인 두 번째 애플리케이션(pid 확인)"
CURRENT_PID2=$(pgrep -f ".*-Dserver.port=$PORT2.*.jar")

log "> 현재 구동중인 두 번째 애플리케이션 pid : $CURRENT_PID2"

if [ -z $CURRENT_PID2 ]
then
  log "> 두 번째 애플리케이션이 구동 중이 아니므로 종료하지 않습니다."
else
  log "> 두 번째 애플리케이션 종료 : kill -15 $CURRENT_PID2"
  kill -15 $CURRENT_PID2
  log "> Waiting for 5 seconds..."
  sleep 5
fi

# 5. 두 번째 인스턴스에 새 버전 배포
log "> 두 번째 인스턴스에 새 버전 배포 (PORT: $PORT2)"
nohup java -jar \
  -Dserver.port=$PORT2 \
  $JAR_NAME > $REPOSITORY/nohup2.out 2>&1 &

# 6. 두 번째 인스턴스 헬스 체크
log "> 두 번째 인스턴스 헬스 체크 시작"

for i in $(seq 1 10)
do
    RESPONSE=$(curl -s http://localhost:$PORT2/health)
    OK_COUNT=$(echo "$RESPONSE" | grep -c 'ok')

    if [ $OK_COUNT -ge 1 ]
    then
        log "> 두 번째 인스턴스가 성공적으로 시작되었습니다."
        break
    else
        log "> 두 번째 인스턴스가 아직 시작되지 않았습니다. 10초 후 다시 시도합니다."
        sleep 10
    fi

    if [ $i -eq 10 ]
    then
        log "> 두 번째 인스턴스가 시작되지 않았습니다. 배포를 중단합니다."
        exit 1
    fi
done

log ">------------------------배포 종료------------------------"

 

 

실제 운영 로그

 

 

 

4. 마무리


L3 또는 L7 스위치를 사용하여 무중단 배포를 할 수 있지만 비용이 들기 때문에, 비용 효율성을 고려하여 하나의 서버에서 여러 개의 애플리케이션 인스턴스를 실행하고 Nginx와 헬스 체크를 활용하여 무중단 배포를 구현했습니다. 이러한 방법을 통해 우리는 서비스 중단 없이 새로운 버전을 배포할 수 있었으며, 예산을 절감하고, 동시에 안정적인 서비스 제공을 가능하게 하는 뛰어난 방법임을 확인할 수 있었습니다.