Profile picture

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

JaehyoJJAng2023년 12월 20일

개요

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

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

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

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

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

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


무중단 배포 ?

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

기존에는 서비스를 업데이트할 때 운영 중인 서비스를 중단하고 업데이트를 진행했어야 했다.

그렇게 되면 중단된 시간 동안 유저들은 서비스에 접속을 하지 못하게되고, 그로 인해 트래픽이 대거 이탈 될 수 있는 큰 단점이 존재한다.

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

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


무중단 배포 종류

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

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

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

이번 포스팅에서는 Blue, Green 무중단 배포에 대해서 실습해보자.


Blue, Green 배포 과정

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

1. 8000번 포트로 연결된 앱 컨테이너
image


2. 새로운 버전의 컨테이너를 8001번 포트로 띄우기
image


3. nginx.conf 파일 수정 후 reload (upstream 8001로 변경)
image


4. 기존 8000번 포트의 컨테이너는 삭제
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...