Profile picture

[Docker] 도커로 무중단 배포하기 (blue, green)

JaehyoJJAng2023년 12월 20일

개요

Honeycam 2024-09-06 15-56-49
도커를 사용해봤다면 서비스 개발 시

로컬에서 테스트를 하기 위해 docker-compose up, docker-compose down등의 명령어를 사용하여

새롭게 컨테이너를 띄우거나 삭제하는 과정들을 진행해봤을거다.

만약 실제 서비스가 배포 중인 production 환경에서 위와 같이 컨테이너를 내렸다 올렸다하면 어떻게 될까?

당연히 컨테이너가 재시작 되는 시점 동안 사용자들이 서비스에 접속을 못하게 될 것이다

그렇기 때문에 실제 production 환경에서는 무중단 배포를 해야한다.


무중단 배포 ?

무중단 배포란 서비스를 업데이트할 때 기존 서비스에 영향을 주지 않고 업데이트 하는 것을 의미한다.

예를 들어, A1이라는 버전의 서비스를 도커 기반으로 운영하고 있었다고 가정해보자.

시간이 흘러 A1 서비스 개발진들은 해당 버전보다 더 개선된 버전으로 A2를 개발 완료하였고 운영 중인 서비스를 A2로 업데이트 하기 위해서 다음과 같은 과정을 거쳤다.
image


그러나 위 구성도처럼 서비스를 배포하게 되면 중단된 시간 동안 유저들은 서비스에 접속을 하지 못하게 되고,

그로 인해 트래픽이 대거 이탈될 수 있는 큰 단점이 존재한다.


이렇게 서비스가 내려가 있는 시간을 '다운 타임(Down time)' 이라고도 하는데

무중단 배포를 도입하면 이러한 다운 타임을 최소화 할 수 있다.


무중단 배포 종류

무중단 배포는 크게 두 가지로 나뉜다.

배포 종류 설명
rolling update 배포 새로 배포되어야 하는 버전을 하나씩 순차적으로 적용시키면서 배포하는 방식.
한 번에 모두 배포되는 게 아니기 때문에 배포가 되는 과정에서 옛날 버전과 새로운 버전이 공존함.
그렇기에 배포 과정 중 호환성 문제가 발생할 수도 있음
Blue, Green 배포 Blue 혹은 Green 버전 둘 중 하나로 배포되어 있는 상태에서 새로운 버전을 동시에 띄우고 로드밸런서를 통해서 스위칭 하는 방식.
한 번에 두개의 버전을 동시에 띄우기 때문에 시스템 자원도 두 배로 든다는 단점이 존재함

또한 위 배포 과정 말고 Canary(카나리) 배포도 있다. 단계적 배포라고도 하며, 일부 사용자들에게만 새로운 버전을 배포하고 안정성을 테스트한다.


각 배포 방식에 대해 자세하게 이해해보자.


Rolling Update

로드밸런서로 연결되어있는 동일한 버전의 서버들을 순차적으로 새로운 버전으로 변환시키는 방식이다.

예를 들어, A1 버전의 애플리케이션을 서버 3대가 배포하고 있다고 가정할 때,

배포되어 있는 서버를 순차적으로 A2 버전으로 교체하는 방식이라고 생각하면 될 것 같다.
image
연결되어있는 서버를 하나씩 멈추고 새로운 버전으로 교체하고 있다.

이 방식의 특징은, 부가적인 서버 자원을 사용하지 않고 배포할 수 있다는 장점이 있지만

배포되는 동안에 발생하는 트래픽에 대한 부하가 더 발생할 수 있다는 단점도 있다.

위 그림의 경우에는 추가적인 서버 자원을 사용하지 않고 배포하는 방법에 대해 표현한 것이지만, 반대되는 방식도 가능하다.

업데이트를 위해 정지된 서버를 대신해서 추가적인 서버를 하나 더 띄우는 것이다.

이렇게 하면 업데이트 때문에 정지된 서버 때문에 발생하는 트래픽 부하가 걸리지 않도록 할 수 있다.


그리고 Rolling Update 에서는 기존 버전과 업데이트 하려는 버전의 호환성이 매우 중요하다

만약 기존 A1 버전에서 유저 목록을 조회하는 /users라는 API가 있는데 A2에서 해당 API가 제거되었다면,

사용자가 유저 목록을 볼 수도 있고, 못 볼 수도 있는 문제가 발생하게 된다.

이런 문제가 발생하는 이유는 다음과 같다.

A2로의 업데이트 과정 중에 A1, A2 서버가 동시에 존재하게 되고, 이 때 유저가 /users API를 호출하게 되면 일부는 응답을 제대로 받고, 일부는 못 받는 문제가 발생하는 것이다.


Blue-Green

Blue-Green 배포 방식은 두 개의 독립적인 Blue 환경과 Green 환경을 가지고 진행되는 배포 전략이라고 한다.

여기서 Blue는 현재 운영 중인 환경을 나타내며, Green은 새로운 버전의 환경을 의미한다.

Green의 배포가 준비되면 Blue 환경에서 Green 환경으로 한 번에 전환한다.
image
A2가 배포된 순간부터 로드 밸런서는 A2로 모든 트래픽을 이동시킨다.

A1의 경우 트래픽 이동의 작업이 끝나는 즉시 종료된다.

이 때, A1A2가 동시에 켜져 있는 순간은 찰나이다.

롤링 배포와는 다르게 한 번에 버전을 업데이트하기 때문에 버전 호환에 대한 문제가 발생하지 않는다.

또, 만약 새로운 버전에 문제가 발생한다면 트래픽을 다시 Blue 환경으로 이전하면 되기 때문에 버전 롤백에 대한 문제도 쉽게 해결 가능하다.

하지만, 짧은 시간 동안이지만 두 버전이 동시에 실행되는 시간이 있기 때문에 추가적인 서버 비용이 발생한다.


Blue, Green 배포 과정

  • 오늘 실습할 Blue, Green 배포 과정을 정리해보자면 아래 순서와 같다.

1. nginx -> 애플리케이션 컨테이너
image


2. 새로운 버전의 컨테이너를 생성
image


3. nginx.conf 파일 수정 후 reload (blue 컨테이너로 트래픽 이동)
image


4. 기존 green 컨테이너는 삭제
image


위 개념을 정리해보자면

새로 배포할 때마다 새로운 컨테이너들을 띄우고

nginx upstream 연결을 기존 컨테이너IP:PORT에서 새로 띄운 컨테이너IP:PORT로 변경 후

이전 컨테이너는 삭제한다.


사전 준비

  • 원활한 실습을 위해 include 속성이 추가된 도커 컴포즈 v2.20.0 버전 이상 필요

무중단 배포 구현

이제 docker-compose.yaml을 작성해보면서 위처럼 무중단 배포를 구현해보자.


docker-compose 작성

  • 실습에서 배포할 앱의 경우 앱의 버전을 리턴하는 아주 간단한 앱이다.

docker-compose.yaml

x-app: &app
  restart: always
  expose:
    - 8000
  environment:
    - APP_VER=1.0

services:
  app-blue:
    build: ./app
    <<: *app
    container_name: app-blue

  app-green:
    build: ./app
    <<: *app
    container_name: app-green

  nginx:
    build: ./nginx
    restart: always
    ports:
      - "80:80"
    volumes:
      - "./nginx/conf.d/:/etc/nginx/conf.d/"
    container_name: nginx

x-appdocker compose 3.4부터 추가된 새로운 포맷이다.

간단하게 설명하자면 blue, green 컨테이너의 공통된 환경 변수를 각각의 컨테이너에 작성하지 않고도 동일하게 사용할 수 있다.

그리고 두 컨테이너는 동일한 이미지를 사용한다.


nginx는 nginx 컨테이너이며, volumes의 conf.d에는 blue와 green 컨테이너로 요청을 보내는 설정 파일이 존재한다.


nginx.conf 작성

nginx.blue.confnginx.green.conf 파일을 작성할 것이다.

여기서 핵심은 두 가지이다.

  • upstream을 사용하여 blue와 green 컨테이너를 가리키고 server 내부의 값은 동일하게 작성한다.
  • 현재 실행 중이지 않은 컨테이너의 파일은 .tmp로 끝나도록 만든다.
    • 이렇게 할 경우 nginx.conf 파일에서 include /etc/nginx/conf.d/*.conf;가 실행될 때 .tmp로 끝나는 파일은 실행되지 않는다.

conf.d/blue.conf

upstream app-blue {
    server app-blue:8000;
}

server {
    listen 80;

    location / {
        proxy_pass http://app-blue;
    }
}

conf.d/green.conf.tmp

upstream app-green {
    server app-green:8000;
}

server {
    listen 80;

    location / {
        proxy_pass http://app-green;
    }
}

nginx.conf 파일에서는 upstreamproxy_pass를 확인해주면 된다.

upstream에서는 컨테이너 이름과 포트를 명시하고 proxy_pass에서는 upstream을 그대로 사용한다.


배포 스크립트 작성

  • 위 두 개의 docker-compose.yaml을 번갈아가며 띄울 수 있는 deploy.sh 스크립트를 작성해보자.

deploy.sh

#!/usr/bin/bash

# require variables
COMPOSE_PATH="/home/$USER/github/blue-green/docker-compose.yaml"
NGINX_CONF_DIR="/home/$USER/github/blue-green/nginx/conf.d"
DELAY=5
NGINX_CONTAINER="nginx"
HEALTH_DELAY=15
HC_ENDPOINT="http://localhost:8000"

# Blue를 기준으로 현재 떠있는 컨테이너 추출 (무조건 한 개의 컨테이너는 실행이 되어있어야 함.)
BLUE_CT="$(docker ps --filter "name=app-blue" --filter "status=running" | grep -v "CONTAINER ID")"

# 컨테이너 스위칭
if [[ -n "$BLUE_CT" ]]; then
    echo "=== BLUE => GREEN ==="

    CURRENT_ENV='app-blue'
    NEW_ENV='app-green'
    CURRENT_NGINX_CONF='blue.conf'
    NEW_NGINX_CONF='green.conf.tmp'
else
    echo "=== GREEN => BLUE ==="

    CURRENT_ENV='app-green'
    NEW_ENV='app-blue'
    CURRENT_NGINX_CONF='green.conf'
    NEW_NGINX_CONF='blue.conf.tmp'
fi

# 새로운 이미지 pull
echo
echo "======================="
echo "Pulling the new env image: $NEW_ENV"
docker-compose -f "$COMPOSE_PATH" pull $NEW_ENV
echo "======================="
echo

# 새로운 이미지 빌드 (캐시 무시)
echo
echo "======================="
echo "Building the new env image: $NEW_ENV with no cache"
docker-compose -f "$COMPOSE_PATH" build --no-cache $NEW_ENV
echo "======================="
echo

# 새로운 컨테이너 시작
echo
echo "======================="
echo "Starting new env: $NEW_ENV"
docker-compose -f "$COMPOSE_PATH" up -d --no-deps $NEW_ENV
echo "======================="
echo

# 컨테이너 시작 대기
sleep $DELAY

# 헬스 체크
echo
echo "======================="
echo "Wating for the new env to be healthy"
for i in $(seq 1 $HEALTH_DELAY); do
    HTTP_STATUS=$(docker-compose -f "$COMPOSE_PATH" exec -T $NEW_ENV curl -s -o /dev/null -w "%{http_code}" $HC_ENDPOINT)
done
echo "======================="
echo

if [[ "$HTTP_STATUS" -ne 200 ]]; then
    echo
    echo "======================="
    echo "New environment did not become healthy in time."
    echo "======================="
    echo
    exit 1
fi

# Nginx conf 업데이트
echo
echo "======================="
echo "Updating nginx configuration"
echo "$NGINX_CONF_DIR/$CURRENT_NGINX_CONF" "$NGINX_CONF_DIR/$CURRENT_NGINX_CONF.tmp"
echo "$NGINX_CONF_DIR/$NEW_NGINX_CONF" "$NGINX_CONF_DIR/${NEW_NGINX_CONF%.tmp}"
echo "======================="
echo

# Nginx 리로드
echo
echo "======================="
echo "Reloading nginx .."
docker-compose -f "$COMPOSE_PATH" exec "$NGINX_CONTAINER" nginx -s reload
echo "======================="
echo

# 이전 환경 중지 및 제거
echo
echo "======================="
echo "Stopping and removing the old environment: $CURRENT_ENV"
docker-compose -f "$COMPOSE_PATH" stop "$CURRENT_ENV"
docker-compose -f "$COMPOSE_PATH" rm -f "$CURRENT_ENV"
echo "======================="
echo

echo
echo "======================="
echo "======================="
echo "배포 완료!"
echo "======================="
echo "======================="
echo

위 스크립트를 간단하게 설명하면 다음과 같다.

  • 현재 실행 중인 컨테이너가 blue인지 green 인지 검사
  • 새로운 이미지 pulling - build
  • 새로운 컨테이너 생성
  • 컨테이너가 기동될 때까지 잠시 대기
  • 명시한 헬스 체크 endpoint가 정상 응답할 때까지 대기
  • .tmp로 끝나는 파일을 .conf로 변경하고 .conf로 끝나는 파일을 .conf.tmp로 변경
  • nginx 컨테이너 reload
  • 이전 환경 중지 및 제거

작동 확인

작성한 스크립트에 실행 권한을 부여하고 스크립트를 실행

$ chmod u+x deploy.sh
$ bash deploy.sh  

image


배포된 애플리케이션은 앱 버전을 출력하는 간단한 서비스이다.

curl 명령을 계속 실행하며 버전이 정상적으로 업데이트되는지 확인해보자.

먼저 docker-compos.yaml 파일에서 APP_VER 속성 값을 2.0으로 변경해주자.

x-app: &app
  restart: always
  expose:
    - 8000
  environment:
    - APP_VER=1.0

...

새로운 터미널 탭을 열고 아래와 같은 코드를 한 줄 작성하자.
while문을 돌며 1초의 대기 시간을 두고 curl 요청을 하는 코드이다.

$ while true; do curl http://localhost; echo ""; sleep 2; done

스크립트가 실행되니 정상적으로 버전이 업데이트 된 것을 볼 수 있다.
Honeycam 2024-09-06 15-56-49


▶︎ 마무리

지금까지 도커를 사용하는 환경에서 무중단 배포를 어떻게 하는지 기록해보았다.

오늘 이해한 내용을 기반으로 컨테이너 오케스트레이션 도구들을 공부해보면 좋을 것 같다.


Loading script...