Haneul's Blog

[스케줄 관리 프로젝트 - 일치(INFRA)] Blue-Green 무중단 배포 본문

일정 관리 프로젝트(일치)

[스케줄 관리 프로젝트 - 일치(INFRA)] Blue-Green 무중단 배포

haneulss 2023. 5. 4. 01:46

무중단 배포란?

말 그대로 배포 중에 서비스 기능은 멈추지 않고, 실제 사용자들이 정상적으로 서비스를 사용할 수 있는 편의를 제공하는 배포 방법입니다.

2개 이상의 서버

그렇다면 이를 위해서는 어떻게 해야 할지를 생각해볼 때 서비스를 다시 배포하기 위해서는 서비스를 중단했다가 다시 실행할 필요성이 있습니다.


이걸 다르게 생각해보면 하나의 서버로는 무중단 배포가 불가능 하다는 것을 알 수 있고, 최소 2개 이상의 서버가 필요합니다.

로드 밸런서의 필요성

만약 로드 밸런서가 없이 무중단 배포를 한다고 생각하면 프론트 측에서는 백엔드의 서버 주소를 모두 알고 있어야 되고, 새로 빌드된 파일을 배포하려고 할 때면 유동적으로 연결된 백엔드 주소를 변경해줘야 하는데 이는 상당히 번거로울 수 있는 작업입니다.
또한 보안, 유지 보수, 안정성의 측면에서도 프론트와 백엔드가 직접 연결되지 않고 로드 밸런서를 두는 편이 좋습니다.

 

그렇기에 무중단 배포를 진행할 때 로드 밸런서를 통해 프론트 측에서는 하나의 서버 주소만 알고 로드 밸런서 측에서 들어오는 요청을 처리하는 환경을 구현해야 합니다.

무중단 배포 종류

롤링(Rolling) 배포

롤링 배포는 사용 중인 인스턴스 내에서 새 버전을 점진적으로 교체하는 방법으로, 서비스 중인 인스턴스 하나 이상을 로드 밸런서에서 라우팅하지 않게 하고, 새 버전을 적용하여 다시 라우팅하는 방법입니다.

 

롤링 배포는 구 버전 신 버전이 같이 동작을 하는 순간이 있기 때문에 호환성 문제가 발생할 수 있습니다.

블루-그린(Blue-Green) 배포

기존 버전(블루)에 연결되어 있던 트래픽을 일괄적으로 신 버전(그린)으로 전환하는 배포 방법으로, 이를 위해서는 신 버전을 미리 배포하고 배포한 신 버전이 정상적으로 동작하는지 확인하는 과정이 필요하며, 신 버전만 라우팅하기 때문에 호환성 문제가 발생하지는 않는다는 장점이 있습니다.

 

하지만 롤링 배포와 달리 구 버전의 서버는 아예 라우팅을 하지 않기 때문에 호환성 문제는 없지만 서버 자원이 2배로 들어가는 단점이 있습니다.

카나리(Canary) 배포

카나리 배포는 롤링 배포와 블루-그린 배포를 합친 배포 방식으로, 신 버전의 대상, 비율등을 관리자 입장에서 직접 조절해가며 배포해가는 방식입니다.

신 버전의 비율을 유동적으로 늘려간다는 부분에서 롤링 배포와 유사하며, 실제 환경에서 미리 테스팅을 진행한다는 점에서 블루-그린 배포와 비슷합니다.

 

그리고 카나리 배포도 롤링 배포와 마찬가지로 신, 구 버전의 호환성을 검증해야 한다는 단점이 있습니다.

내 프로젝트에 적용할 무중단 배포 방식

제 개인적으로는 호환성 문제로 인해서 롤링 배포와, 카나리 배포를 선호하지 않기 때문에 호환성을 생각할 필요가 없는 Blue-Green을 이번 프로젝트에 적용하기로 하였습니다.

Blue-Green 동작 방법

1. Nginx 설치

// 설치
sudo apt update
sudo apt install nginx

2. Nginx 리버스 프록시 설정

리버스 프록시 설정을 위해서 /etc/nginx/conf.d 위치로 이동을 하고, default.conf 파일을 생성합니다.

cd /etc/nginx/conf.d
vim default.conf

생성한 default.conf 파일에 아래의 내용을 넣어줍니다.

server {
    listen 80;
    server_name 도메인주소;

    location / {
        proxy_pass http://localhost:포트번호;
        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;
    }
}

3. Certbot 설치 및 Let's Encrypt에서 SSL 인증서 발급

Cerbot을 통해서 SSL인증서를 발급 받을 수 있으며 이를 발급 Cerbot은 snap 패키지 매니저를 통해 설치하는 것이 권장되어서 아래와 같이 snap을 통해 설치합니다.

// snap 패키지 매니저 설치
sudo snap install certbot --classic

// SSL 인증서 발급
sudo certbot --nginx

위의 과정을 거치고 다시 default.conf를 확인해본다면 HTTPS를 위한 여러 설정이 자동으로 추가되어 있습니다.

4. Nginx default.conf 파일 수정

무중단 배포를 위한 변수를 위해 사용할 $service_url 변수를 service-url.inc 파일에 설정해둡니다.

vi /etc/nginx/conf.d/service-url.inc

# 추후에 사용할 애플리케이션 변수 작성
set $service_url http://127.0.0.1:8081;

/etc/nginx/conf.d/default.conf 파일에서 위에서 만든 변수를 활용하여 추후에 무중단 배포를 할 수 있도록 설정합니다.

// sercice-url.inc 파일에서 $service_url 변수 가져오기
include /etc/nginx/conf.d/service-url.inc;
location / {
        // service_url 변수로 리버스 프록시 설정하기.
        proxy_pass $service_url;
        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;
}

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

Nginx

profile.sh

현재 nginx와 연결되지 않은 port 찾을 수 있도록 도움을 주는 함수를 모아둔 스크립트입니다.

#!/usr/bin/env bash

function find_idle_profile()
{
        RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://{도메인명}/api/profile)
        if [ ${RESPONSE_CODE} -ge 400 ]

        then
                CURRENT_PROFILE=port2
        else
                CURRENT_PROFILE=$(curl -s https://{domain명}/api/profile)
        fi

        if [ ${CURRENT_PROFILE} == port1 ]
        then
                IDLE_PROFILE=port2
        else
                IDLE_PROFILE=port1
        fi

        echo "${IDLE_PROFILE}"
}

function find_idle_port()
{
        IDLE_PROFILE=$(find_idle_profile)

        if [ ${IDLE_PROFILE} == port1 ]
        then
                echo "8081"
        else
                echo "8082"
        fi
}

stop.sh

profile.sh를 활용하여 쉬고 있는 포트의 작동을 멈추게 합니다.

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

IDLE_PORT=$(find_idle_port)

echo "> $IDLE_PORT에서 구동중인 애플리케이션 PID 확인"
IDLE_PID=$(sudo lsof -ti tcp:${IDLE_PORT})

if [ -z ${IDLE_PID} ]
then
        echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $IDLE_PID"
        kill -15 ${IDLE_PID}
        sleep 5
fi

start.sh

nginx와 연결되지 않은 port로 springboot 애플리케이션을 시작합니다.

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

REPOSITORY=/home/ubuntu/app
PROFJECT_NAME={프로젝트명}

IDLE_PORT=$(find_idle_port)

echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME에 실행권한 추가"

chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"

IDLE_PROFILE=$(find_idle_profile)

echo "> $JAR_NAME을 porfile=$IDLE_PROFILE로 실행"

nohup java -jar -Dspring.config.location:$REPOSITORY/application.yml -Dspring.profiles.active=$IDLE_PROFILE  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

switch.sh

현재 nginx와 연결되지 않은 port를 nginx와 연결하도록 변경합니다.

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

function switch_proxy()
{
        IDLE_PORT=$(find_idle_port)

        echo "> 전환할 Port: $IDLE_PORT"
        echo "> Port 전환"
        echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

        echo "> 엔진엑스 Reload"
        sudo service nginx reload
}

health.sh

nginx와 연결되지 않은 Port가 현재 작동중인지 보고 작동이 되어있다면 위의 Switch.sh를 활용해서 Nginx와 연결된 Port를 바꿔주고, 10번까지 시도해보고 실패한다면 기존에 nginx에 연결되어 있던 port를 그대로 둡니다.

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)

source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh

IDLE_PORT=$(find_idle_port)

echo "> Health Check Start!"
echo "> IDLE_PORT: $IDLE_PORT"
echo "> curl -s http://localhost:$IDLE_PORT/api/profile "
sleep 10

for RETRY_COUNT in {1..10}
do
        RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/api/profile)
        UP_COUNT=$(echo ${RESPONSE} | grep 'port' | wc  -l)

        if [ ${UP_COUNT} -ge 1 ]
        then
                echo "> Health check 성공"
                switch_proxy
                break
        else
                echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다"
                echo "> Health check: ${RESPONSE}"
        fi

        if [ ${RETRY_COUNT} -eq 10 ]
        then
                echo "> Health check 실패 "
                echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
                exit 1
        fi
        echo "> Health check 연결 실패. 재시도..."
        sleep 10
done

deploy.sh

위의 스크립트들의 활용하여 무중단 배포를 실질적으로 이루어주는 스크립트입니다.

# nginx와 연결되지 않은 PORT 죽이기
sh stop.sh
# 새로 빌드한 jar 파일을 방금 위에서 죽인 Port로 시작
sh start.sh
# start.sh 과정을 통해 실행된 Port가 실행된다면 nginx의 연결을 바꿈
sh health.sh

Reference