무중단 배포란?
무중단 배포(Zero Downtime Deployment)는 애플리케이션을 배포하는 동안 서비스의 가용성을 유지하면서 사용자 경험에 영향을 주지 않는 배포 방식이다. 일반적으로 서비스 중단 없이 새로운 애플리케이션 버전을 릴리스하기 위해 여러 배포 전략이 사용됩니다. 그중 블루그린 배포는 간단하면서도 효과적인 방식으로 널리 사용된다. 블루그린 배포 방식 이외에도 여러가지 방식이 있는데, 이 게시글에 자세하게 나와있으니 참고하면 좋을 것 같다.
블루/그린 배포 방식을 선택한 이유
대중적인 배포 방식 세가지를 비교해 보자
배포 전략특징
롤링 배포 | 기존 환경에서 하나씩 새로운 버전으로 업데이트. 일부 인스턴스는 기존 버전, 일부는 새 버전을 동시에 실행. | - 점진적 배포로 리소스 사용 효율적 - 대규모 서비스에 적합. |
- 배포 중 문제 발생 시 롤백 어려움 - 혼합 상태에서의 테스트 어려움 |
블루그린 배포 | 두 개의 독립적인 환경(블루, 그린)을 사용하여 하나는 가동 중, 하나는 새 버전 배포 후 트래픽 전환. | - 빠른 롤백 가능 - 사용자 영향 최소화. |
- 두 환경 유지로 리소스 소비 많음 - 설정 및 관리 복잡 |
카나리 배포 | 새 버전을 소수의 사용자에게만 배포 후 점진적으로 배포 범위를 확대. | - 문제 조기 발견 가능 - 점진적 위험 감소 - 사용자의 피드백 기반 배포 |
- 배포 속도가 느림 - 트래픽 관리 및 모니터링 복잡 |
위 테이블에 있는 장단점을 분석한 결과, 가장 적합하다고 생각되는 배포 방식을 선정했다.
무중단 배포를 처음 도입하게 되면서 여러 테스트를 진행할 필요가 있었으며, 배포가 되는 시점을 즉각적으로 확인할 필요가 있었다. 또한 많은 트래픽이 발생하는 서버가 아니기 때문에, 점진적으로 배포가 되는 방식을 채택하는 것은 매력적으로 다가오지 않았다.
무엇보다 블루/그린이 참고문서가 가장 많았다.
블루그린 배포란?
블루그린 배포(Blue-Green Deployment)는 두 개의 독립적인 환경(블루와 그린)을 활용하여 하나는 현재 가동 중인 환경으로, 다른 하나는 새 버전을 배포할 환경으로 설정하는 방식이다. 배포 후 새 환경이 성공적으로 작동하면 트래픽을 새 환경으로 전환한다.
플로우 :
최초에 Nginx에서는 Running 상태인 블루 서버를 포워딩하고 있다고 가정한다. 또한 Jenkins에서 빌드 및 테스트하는 과정은 생략한다.
1. 멈춰있는 상태의 그린 서버에 최신 릴리즈 내용을 배포
2. 배포 후 헬스 체크를 통해 그린 서버가 정상적 Response를 보내는지 체크
3. 이상 없을 시 Nginx의 프록시 경로를 그린 서버로 변경
4. 블루 서버는 멈춤 상태로 전환
위와 같은 순서로 진행되며, 한번 더 배포가 발생된다면 위 (1) ~ (4) 내용의 그린 서버를 블루로 치환하면 된다.
배포 스크립트는?
많은 부분을 구글링의 도움으로 작성했지만, 서비스 인프라 혹은 서버 스펙에 맞춰 수정했다.
코드:
cp /jar/service-app*.jar /jar/app.jar
rm -f /jar/service-app*.jar
# 현재 날짜 및 시간을 로그 파일에 기록
echo "Deployment started at $(date)" >> $LOG_FILE
{
# 기존 latest 이미지를 previous로 태그 재할당
docker tag ${LATEST_IMAGE_TAG} ${PREVIOUS_IMAGE_TAG}
docker build -t ${LATEST_IMAGE_TAG} /jar
if [ -n "$RUNNING_APPLICATION" ]; then
echo "green Deploy..."
docker-compose -f /bin/docker-compose.yaml up -d green
while true; do
echo "green health check...."
REQUEST=$(docker exec nginx curl http://${SERVER_URL}:${HTTPS_GREEN_PORT})
if [ -n "$REQUEST" ]; then
HEALTH_CHECK_PASSED=true
break
fi
ELAPSED=$(($SECONDS - $START))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "타임아웃 발생"
break
fi
sleep 3
done;
if [ "$HEALTH_CHECK_PASSED" = true ]; then
sed -i 's/blue-server/green-server/g' $DEFAULT_CONF
docker exec nginx service nginx reload
docker-compose -f /bin/docker-compose.yaml stop blue
docker rmi $(docker images -f "dangling=true" -q)
else
echo "헬스 체크 실패"
fi
else
echo "blue Deploy..."
docker-compose -f /bin/docker-compose.yaml up -d blue
while true; do
echo "blue health check...."
REQUEST=$(docker exec nginx curl http://${SERVER_URL}:${HTTPS_BLUE_PORT})
if [ -n "$REQUEST" ]; then
HEALTH_CHECK_PASSED=true
break
fi
ELAPSED=$(($SECONDS - $START))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "타임아웃 발생"
break
fi
sleep 3
done;
if [ "$HEALTH_CHECK_PASSED" = true ]; then
sed -i 's/green-server/blue-server/g' $DEFAULT_CONF
docker exec nginx service nginx reload
docker-compose -f /bin/docker-compose.yaml stop green
docker rmi $(docker images -f "dangling=true" -q)
else
echo "헬스 체크 실패"
fi
fi
} 2>&1 | tee -a $LOG_FILE
echo "Deployment finished at $(date)" >> $LOG_FILE
코드 설명
이전 이미지 관리
$ docker tag ${LATEST_IMAGE_TAG} ${PREVIOUS_IMAGE_TAG}
현재 실행 중인 이미지를 이전 버전으로 태그 재할당하여 롤백 시 사용할 수 있도록 준비한다.
이미지 빌드
$ docker build -t ${LATEST_IMAGE_TAG} /jar
새로운 애플리케이션 이미지를 빌드한다.
배포 및 헬스 체크 (그린 환경)
$ docker-compose -f /bin/docker-compose.yaml up -d green
그린 서버에 새로운 버전을 배포하고 헬스 체크를 통해 정상 작동 여부를 확인한다.
while true; do
REQUEST=$(docker exec nginx curl http://${SERVER_URL}:${HTTPS_GREEN_PORT})
if [ -n "$REQUEST" ]; then
HEALTH_CHECK_PASSED=true
break
fi
ELAPSED=$(($SECONDS - $START))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "타임아웃 발생"
break
fi
sleep 3
done;
새로운 버전의 헬스 체크가 성공하면 플래그를 설정하고, 실패하거나 타임아웃이 발생하면 종료한다.
트래픽 전환
$ sed -i 's/blue-server/green-server/g' $DEFAULT_CONF docker exec nginx service nginx reload
Nginx 설정 파일을 수정하여 트래픽을 블루에서 그린으로 전환한다
블루 환경 정리
$ docker-compose -f /bin/docker-compose.yaml stop blue docker rmi $(docker images -f "dangling=true" -q)
이전 블루 환경을 중지하고, 사용하지 않는 이미지를 삭제하여 리소스를 정리한다.
로그 기록 종료
$ echo "Deployment finished at $(date)" >> $LOG_FILE
배포 종료 시간을 기록하여 전체 프로세스를 문서화한다.
정리하며
구현한 방식에는 잠재적인 위험요소가 존재한다
만약, 멈춤 상태의 서버에 헬스 체크가 정상적으로 Response를 반환하더라도 서버 내부적으로 문제가 발생한다면 어떻게 해야 할까? 위 헬스 체크는 특정 엔드포인트를 호출하여 HTTP 상태 코드(200)를 확인하고 있는데, 이 엔드포인트에서 DB 연결 상태, 디스크 상태, 또는 앱의 주요 로직 등을 확인하지 않는다면, 내부적인 결함을 인식하지 못할 수 있다.
또한 헬스 체크 시점과 에러 발생 시점의 시간차가 있을 수 있다는 잠재적 위험이 있다.
해결방안?
헬스 체크 하는 엔드포인트에서 DB 커넥션, 디스크 상태 등을 확인할 수 있게 하면, 첫 번째 문제의 리스크는 어느 정도 감소할 것 같다. 또한 배포 후 유예 시간을 설정하고 일정 시간 동안 서버의 안정성을 확인하는 방법으로 두 번째의 리스크도 감소시킬 수 있을 것으로 기대한다(카나리 배포의 장점).
앞으로는
첫 번째, 블루/그린 배포 방식(내가 구현한)에 존재하는 잠재적인 리스크 제거.
두 번째, 오류를 조기에 식별할 수 있는 카나리 배포 방식 구현.
위 두 가지를 순차적으로 진행하고자 한다.