블루 그린 경험해보기

심규민·2024년 3월 3일
1

최근에 큐시즘에서 무중단 배포 관련해서 강의를 진행한적이 있었다.
하지만 정작 나는 무중단 배포를 경험해본적이 없어 이번 큐시즘 기업 프로젝트에 적용해보기로 했다.

무중단 배포란?

우선 들어가기에 앞서 무중단 배포에 대해서 이야기해보려 한다.

무중단 배포는 이름 그래도 서비스의 중단 없이 배포를 진행함을 의미한다.

즉, CD를 통해 배포를 진행하다보면 필연적으로 구 버전의 서비스(컨테이너)가 종료되고 새 버전의 서비스가 실행되는데, 종료되고 시작되는 그 사이에 사용자가 해당 서비스를 이용하지 못하는 상황이 발생한다. 이러한 중단 시간을 최소화하고자 무중단 배포를 사용한다.

블루 그린이란?

블루 그린 방식은 구버전을 의미하는 블루와 신버전을 의미하는 그린 두 그룹이 존재한다.
배포가 진행되면 신버전을 가진 그린이 시작이 되며 기존 요청들은 그래도 블루 그룹에 전달된다.
그 후 그린의 인스턴스들이 요청을 받을 수 있는 상태가 되면 로드밸런서를 블루에서 그린으로 전환하여 다운타임을 최소화하는 방법이다.

블루 그린 방식은 로드밸런서를 재조정하는 시간으로 인해 짧은 시간이 다운타임이 필연적으로 발생하는 단점을 가지고 있다. 또한 블루 그룹과 그린 그룹을 각각의 인스턴스로 관리한다면 일반 배포의 2배의 리소스를 사용하게 되는 단점을 가지고 있다.

블루 그린 쉘 스크립트로 구현해보기

블루 그린을 구현할때 나는 단일 인스턴스에서 블루, 그린 애플리케이션을 둘 다 띄우는 방식을 통해 구현했다.

블루 그린을 구현하기에 앞서 동작 순서를 나열하면 다음과 같이 생각할 수 있다.

  1. 기존 블루 애플리케이션의 포트를 확인한다. (환경 변수 등등)
  2. 블루 포트에 따라 그린 포트번호를 할당해준다.
    • 블루 포트가 8081이면 그린을 8082포트에 할당
    • 블루 포트가 8082이면 그린을 8081포트에 할당
  3. 결정된 그린 포트로 그린 애플리케이션을 실행시킨다.
  4. 실행시킨 그린 애플리케이션의 health check api의 응답값이 정상으로 돌아오는지 확인한다.
    5.1 정상으로 돌아온다면 기존 블루 컨테이너의 이름을 삭제 예정 이름으로 변경하고 그린 컨테이너의 이름을 블루 이름으로 변경, 그 다음 로드밸런서를 reload하여 변경된 블루 컨테이너를 재설정하도록 해준다. 하지막으로 기존 삭제 예정으로 변경된 컨테이너를 내린다.
    5.2 응답값이 정상으로 들어오지 않는다면 그린 컨테이너를 내려 롤백을 진행한다.

1. 블루 애플리케이션 포트 확인하기

우선 나는 현재 블루 애플리케이션의 포트를 .env파일을 통해 관리하고 있다.
물론 도커를 사용하고 있으니 docker port 명령어를 통해 확인할 수 있지만 간단하게 구현하기 위해서 .env 파일을 사용했다.

source .env

current_application_port=$application_port

echo ${current_application_port}

2. 블루 포트에 따라 그린 포트번호를 할당해준다.

1번의 과정에 따라 얻은 블루 애플리케이션의 포트를 통해 다음과 같은 조건으로 그린 애플리케이션 포트를 정한다.

  • 블루가 8081 포트를 사용하면 그린은 8082 포트를 할당해준다.
  • 블루가 8082 포트를 사용하면 그린은 8081 포트를 할당해준다.
green_application_port=0
green_application_name="green-application"
blue_application_name="blue-application"

...

if [ "$current_application_port" -eq 8081 ]; then
        green_application_port=8082
else
        green_application_port=8081
fi

3. 결정된 그린 포트로 그린 애플리케이션을 실행시킨다.

2번의 과정을 통해 그린 포트가 정해진다면 해당 포트로 애플리케이션을 실행시킨다.

echo $(docker pull ${image_name})

echo $(docker run -d -p ${green_application_port}:8080 --name ${green_application_name} --net application --env-file /home/ubuntu/.env  ${image_name})

echo $(docker image prune -f)

4. 실행시킨 그린 애플리케이션의 health check api의 응답값이 정상으로 돌아오는지 확인한다.

기본적으로 애플리케이션에는 /health uri로 애플리케이션의 상태를 조회할 수 있다. 그래서 curl을 통해 해당 api의 응답값의 http code를 저장하고 해당 코드가 202(ACCEPTED)이면 애플리케이션이 정상적으로 동작한다 판단하여 for를 탈출하도록 구성했다.
만약 응답 코드가 202가 아니면 애플리케이션이 시작 중이거나, 실행에 실패하였기에 우선 10초를 기다리고 이를 10번(100초)를 기다린다.

application_status="FAIL"

for i in {1..10}; do
        cmd=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${green_application_port}/health)

        echo ${cmd}

        if [ "$cmd" -eq 202 ]; then
                application_status="SUCCESS"
                echo ${application_status}
                break;
        else
                echo "FAIL"
                sleep 10
        fi
done

5.1 그린이 정상적으로 실행된 경우

만약 앞서 상태를 확인했을 때 정상적으로 202가 반환이 됐다면 다음과 같은 순서로 블루에서 그린으로 변경한다.

  1. 이미지를 한 번 정리한다.
  2. 블루 컨테이너의 이름을 임시 애플리케이션 이름(deprecated_application)으로 변경한다.
  3. 그린 컨테이너의 이름을 블루(blue_application)로 변경한다.
  4. 환경 변수에 그린 애플리케이션의 포트를 저장한다.
  5. 로드밸런서를 reload를 해준다.
  6. 기존 블루 애플리케이션을 제거한다.
echo "reload processing"
echo $(docker image prune -f)
echo $(docker rename $blue_application_name $temp_application_name)
echo $(docker rename $green_application_name $blue_application_name)
sed -i "s/application_port=.*/application_port=$green_application_port/" .env
echo $(docker exec -i nginx-nginx-1 service nginx reload)
echo $(docker rm -f $temp_application_name)

5.2 그린이 정상적으로 실행되지 않은 경우

그린 애플리케이션이 정상적으로 실행되지 않은 경우 실행 중인 그린 컨테이너를 종료한다.

echo "application unhealty"
$(docker rm -f $green_application_name)

만들어보니

처음으로 쉘 스크립트를 직접 작성하여 블루 그린을 구현해보니 여러 어려운점들이 있었다.

github action source 커맨드

원래는 .env 파일이 아닌 export를 통한 환경 변수를 관리하려 했다. 그리고 스크립트 내부에 export를 사용하기 위해서는 source를 통해 스크립트를 실행시켜야 했다. 하지만 github action에서 source를 이용해서 스크립트를 실행시켜도 sh, bash를 이용한 방법과 동일하게 동작했다. 즉, 환경 변수를 제어하지 못했다..... 그래서 임시로 .env를 통해 블루 애플리케이션의 포트를 관리했다.

docker exec -it

도커 커맨드를 실행시킬 때, 컨테이너에 커맨드를 실행시킬 수 있는 exec를 많이 사용한다. 이때 -i와 -t를 같이 사용을 하는데, github action에서 스크립트를 실행할 때, -t를 이용하면 문제가 발생했다. 하지만 이유를 알 수 없었다....

i는 –interactive 옵션으로 STDIN(표준입력)으로 컨테이너를 생성 하라는 뜻이다.
t는 –tty 옵션으로 영어로 Allocate a pseudo-TTY 라고 설명이 되어있는데
여기서 pseudo-TTY는 유사 터미널로 컨테이너에 터미널 드라이버를 추가하여 컨테이너를 터미널을 이용하여 연결 할 수있도록 하는 옵션이다.
즉, i옵션으로 표준 입력을 받으며 t옵션으로 터미널로 연결 가능한 컨테이너를 만드는 것이다.
(출처 - https://snowturtle93.github.io/posts/Docker-Run-옵션/)

전체 스크립트

#! /bin/bash

source .env

current_application_port=$application_port

echo ${current_application_port}

green_application_port=0
green_application_name="green-application"
blue_application_name="blue-application"
temp_application_name="deprecated-application"

image_name=$image_name

if [ "$current_application_port" -eq 8081 ]; then
        green_application_port=8082
else
        green_application_port=8081
fi

echo ${green_application_port}


echo $(docker pull ${image_name})

cmd=$(docker run -d -p ${green_application_port}:8080 --name ${green_application_name} --net application --env-file /home/ubuntu/.env  ${image_name})

echo ${cmd}

echo $(docker image prune -f)

application_status="FAIL"

for i in {1..10}; do
        cmd=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${green_application_port}/health)

        echo ${cmd}

        if [ "$cmd" -eq 202 ]; then
                application_status="SUCCESS"
                echo ${application_status}
                break;
        else
                echo "FAIL"
                sleep 10
        fi
done

if [ "$application_staus" == "FAIL" ]; then
        echo "application unhealty"
        $(docker rm -f $green_application_name)
else
        echo "reload processing"
        echo $(docker image prune -f)
        echo $(docker rename $blue_application_name $temp_application_name)
        echo $(docker rename $green_application_name $blue_application_name)
        sed -i "s/application_port=.*/application_port=$green_application_port/" .env
        echo $(docker exec -i nginx-nginx-1 service nginx reload)
        echo $(docker rm -f $temp_application_name)
fi

0개의 댓글