최근에 큐시즘에서 무중단 배포 관련해서 강의를 진행한적이 있었다.
하지만 정작 나는 무중단 배포를 경험해본적이 없어 이번 큐시즘 기업 프로젝트에 적용해보기로 했다.
우선 들어가기에 앞서 무중단 배포에 대해서 이야기해보려 한다.
무중단 배포는 이름 그래도 서비스의 중단 없이 배포를 진행함을 의미한다.
즉, CD를 통해 배포를 진행하다보면 필연적으로 구 버전의 서비스(컨테이너)가 종료되고 새 버전의 서비스가 실행되는데, 종료되고 시작되는 그 사이에 사용자가 해당 서비스를 이용하지 못하는 상황이 발생한다. 이러한 중단 시간을 최소화하고자 무중단 배포를 사용한다.
블루 그린 방식은 구버전을 의미하는 블루와 신버전을 의미하는 그린 두 그룹이 존재한다.
배포가 진행되면 신버전을 가진 그린이 시작이 되며 기존 요청들은 그래도 블루 그룹에 전달된다.
그 후 그린의 인스턴스들이 요청을 받을 수 있는 상태가 되면 로드밸런서를 블루에서 그린으로 전환하여 다운타임을 최소화하는 방법이다.
블루 그린 방식은 로드밸런서를 재조정하는 시간으로 인해 짧은 시간이 다운타임이 필연적으로 발생하는 단점을 가지고 있다. 또한 블루 그룹과 그린 그룹을 각각의 인스턴스로 관리한다면 일반 배포의 2배의 리소스를 사용하게 되는 단점을 가지고 있다.
블루 그린을 구현할때 나는 단일 인스턴스에서 블루, 그린 애플리케이션을 둘 다 띄우는 방식을 통해 구현했다.
블루 그린을 구현하기에 앞서 동작 순서를 나열하면 다음과 같이 생각할 수 있다.
우선 나는 현재 블루 애플리케이션의 포트를 .env파일을 통해 관리하고 있다.
물론 도커를 사용하고 있으니 docker port 명령어를 통해 확인할 수 있지만 간단하게 구현하기 위해서 .env 파일을 사용했다.
source .env
current_application_port=$application_port
echo ${current_application_port}
1번의 과정에 따라 얻은 블루 애플리케이션의 포트를 통해 다음과 같은 조건으로 그린 애플리케이션 포트를 정한다.
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
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)
기본적으로 애플리케이션에는 /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
만약 앞서 상태를 확인했을 때 정상적으로 202가 반환이 됐다면 다음과 같은 순서로 블루에서 그린으로 변경한다.
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)
그린 애플리케이션이 정상적으로 실행되지 않은 경우 실행 중인 그린 컨테이너를 종료한다.
echo "application unhealty"
$(docker rm -f $green_application_name)
처음으로 쉘 스크립트를 직접 작성하여 블루 그린을 구현해보니 여러 어려운점들이 있었다.
원래는 .env 파일이 아닌 export를 통한 환경 변수를 관리하려 했다. 그리고 스크립트 내부에 export를 사용하기 위해서는 source를 통해 스크립트를 실행시켜야 했다. 하지만 github action에서 source를 이용해서 스크립트를 실행시켜도 sh, bash를 이용한 방법과 동일하게 동작했다. 즉, 환경 변수를 제어하지 못했다..... 그래서 임시로 .env를 통해 블루 애플리케이션의 포트를 관리했다.
도커 커맨드를 실행시킬 때, 컨테이너에 커맨드를 실행시킬 수 있는 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