개발하는 자몽

블루/그린 무중단 배포 본문

Architecture & Tool

블루/그린 무중단 배포

jaamong 2024. 2. 24. 09:07

목표

Nginx와 Docker를 이용하는 블루/그린 무중단 배포 방식을 정리합니다. 마지막을 보면 나와있지만, 사실 이번 블루/그린 무중단 배포는 실패입니다. 하지만, 기록 겸 회고하기 위해 남깁니다.

 

 

무중단 배포 Zero-downtime Deployment

무중단 배포는 서비스가 중단되지 않은 상태(zero-downtime)에서 새로운 버전을 사용자들에게 배포하는 것입니다. 무중단 배포에는 크게 3가지 방법이 있습니다.

 

1. 롤링 배포 Rolling Deployment

트래픽을 점진적으로 구버전(사용 중인 버전)에서 새로운 버전으로 교체하는 배포 전략입니다. 서비스 중인 서버 하나를 로드밸런서에서 라우팅 하지 않도록 한 뒤, 새 버전을 적용하여 다시 라우팅 하도록 합니다. 이를 반복하여 모든 서버에 새 버전을 배포합니다.

 

🔹 장점

  • 추가적인 서버를 필요로 하지 않음
  • 서버마다 차례로 배포를 진행하기 때문에 상황에 따라 쉽게 롤백(Rollback) 가능

🔹 단점

  • 새 버전을 배포할 때, 서버의 수가 감소하므로 사용 중인 서버에 트래픽이 몰릴 수 있음 → 따라서 서비스 처리 용량을 고려해야 함
  • 배포 진행 시 구버전과 신버전이 공존하므로 호환성 문제가 발생할 수 있음 → 이로 인해 사용자들이 균일한 서비스를 이용할 수 없음

 

2. 블루/그린 배포 Blue/Green Deployment

blue는 구버전(현버전)이고, green은 새로운 버전을 의미합니다. 운영 환경에 구버전과 동일하게 새로운 버전의 서버를 구성한 후 로드밸런서를 통해 새로운 버전으로 모든 트래픽을 전환하는 배포 전략입니다.

 

🔹 장점

  • 구버전과 동일한 운영환경으로 새로운 버전의 서버를 구성하므로 실제 서비스 환경에서 새로운 버전을 테스트할 수 있음
  • 손쉽게 롤백(Rollback) 가능
  • 배포 완료 후 남아 있는 기존 버전의 환경을 다음 배포에 재사용할 수 있음

🔹 단점

  • 시스템 자원이 두 배로 필요함
  • 새로운 환경에 대한 테스트가 전제되어야 함

 

3. 카나리 배포 Canary Deployment

옛날 광부들이 유독 가스에 민감한 카나리아 새를 이용해 가스 누출 위험을 감지했던 것에서 유래한 것으로 잠재적 문제 상황을 미리 발견하기 위한 배포 전략입니다. 새로운 버전의 제공 범위를 늘려가면서 모니터링 및 피드백 과정을 거칠 수 있습니다.

로드밸런서를 통해 새로운 버전의 제품을 경험하는 사용자를 조절할 수 있는 것이 특징으로, 새로운 버전을 특정 사용자(ex. 모바일 이용자) 또는 단순 비율에 따라 구분해 제공할 수 있습니다.

해당 방식은 신버전 배포 전에 실제 운영 환경에서 미리 테스트하는 점이 블루/그린 배포와 비슷합니다. 하지만 카나리 배포는 단계적인 전환 방식을 통해 부정적 영향을 최소화하고 상황에 따라 트래픽 양을 늘리거나 롤백할 수 있습니다.

 

🔹 장점

  • 단계적인 전환 방식을 통해 부정적 영향을 최소화하고 상황에 따라 트래픽 양을 늘리거나 롤백할 수 있음

🔹 단점

  • 롤링 배포와 마찬가지로 신·구 두 버전이 운영되므로 버전 관리 필요

 

 

Blue/Green 배포

1. docker-compose 작성

`docker-compose.green.yml`과 `docker-compose.blue.yml`, `docker-compose.nginx.yml`을 아래와 같이 작성합니다.

 

docker-compose.green.yml

version: "3.5"

services:
  _build_image:
    image: project/server
    build: .

  green_server1:
    image: project/server
    container_name: project_green_1
    restart: always
    command: gunicorn django_server.backend.wsgi --bind 0.0.0.0:4100 --log-level debug --timeout=120
    ports:
      - "4100"  
    depends_on:
      - _build_image
	...

  green_server2:
    image: project/server
    container_name: project_green_2
    restart: always
    command: gunicorn django_server.backend.wsgi --bind 0.0.0.0:4100 --log-level debug --timeout=120
    ports:
      - "4100"  
    depends_on:
      - _build_image
  	...

volumes:
  media-vol:
    external: true  # 볼륨 생성 X, 외부에서 생성한 볼륨 사용

 

 

docker-compose.blue.yml

version: "3.5"

services:
  _build_image:
    image: project/server
    build: .

  blue_server1:
    image: project/server
    container_name: project_blue_1
    restart: always
    command: gunicorn django_server.backend.wsgi --bind 0.0.0.0:4100 --log-level debug --timeout=120
    ports:
      - "4100"  
    depends_on:
      - _build_image
	...

  blue_server2:
    image: project/server
    container_name: project_blue_2
    restart: always
    command: gunicorn django_server.backend.wsgi --bind 0.0.0.0:4100 --log-level debug --timeout=120
    ports:
      - "4100"  
    depends_on:
      - _build_image
  	...

volumes:
  media-vol:
    external: true  # 볼륨 생성 X, 외부에서 생성한 볼륨 사용

 

 

docker-compose.nginx.yml

아래의 Nginx 컨테이너는 컨테이너 스위칭과 상관없이 계속 돌도록 하기 위해 기존 docker compose 파일에서 분리합니다.

version: "3.5"

services:
  nginx:
    image: project/nginx   
    container_name: project_nginx
    build: ./nginx  
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # read-only
      - media-vol:/media/  # django의 media를 사용할 수 있도록 volume 지정
    ports:
      - "4100:80"

volumes:
  media-vol:
    external: true  # 볼륨 생성 X, 외부에서 생성한 볼륨 사용

 

 

이제 blue와 green의 `docker-compose.yml`을 번갈아가며 띄울 수 있는 `deploy.sh`를 작성합니다.

 

2. deploy.sh 작성

#!/bin/bash

# Blue를 기준으로 현재 실행 중인 컨테이너 체크
EXIST_BLUE=$(docker ps | grep project_blue)
DEFAULT_CONF="/nginx/nginx.conf"

# 컨테이너 스위칭
if [ -z "$EXIST_BLUE" ]; then  
    echo "Deploy Blue..."
    docker-compose -f docker-compose.blue.yml up --build -d
    BEFORE_COMPOSE_COLOR="Green"
    AFTER_COMPOSE_COLOR="Blue"
else
    echo "Deploy Green..."
    docker-compose -f docker-compose.green.yml up --build -d
    BEFORE_COMPOSE_COLOR="Blue"
    AFTER_COMPOSE_COLOR="Green"
fi

sleep 2m 30s

# 새로운 컨테이너가 정상적으로 운영 중인지 확인
EXIST_AFTER=$(docker-compose -f docker-compose.$(AFTER_COMPOSE_COLOR).yml ps | grep Up)
if [ -n "EXIST_AFTER" ]; then
		
    # 정상적으로 운영 중이라면 이전 컨테이너 종료
    docker-compose -f docker-compose.${BEFORE_COMPOSE_COLOR}.yml down
    echo "container ${BEFORE_COMPOSE_COLOR} down"
fi

 

 

이제 각 컨테이너 포트로 연결해 주는 Nginx의 설정을 수정해야 합니다.

 

3. nginx.conf 작성

`nginx.conf` 파일 또한 blue와 green으로 나누어서 작성하고 동일한 위치에 둡니다. blue와 green 컨테이너가 다르므로 그에 맞춰서 `upstream` 설정을 변경합니다.

 

nginx.blue.conf

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    ...

    # for load balancing
    upstream project_server {
        server blue_server1:4100;
        server blue_server2:4100;
        keepalive 1024;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://project_server/;    
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            # for 504 error
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
            send_timeout 300s;
        }

        ...
    }
}

 

nginx.green.conf

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    ...

    # for load balancing
    upstream project_server {
        server green_server1:4100;
        server green_server2:4100;
        keepalive 1024;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://project_server/;   
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            # for 504 error
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
            send_timeout 300s;
        }

        ...
    }
}

 

4. deploy.sh 추가 수정

도커 컨테이너가 정상적으로 스위칭되어 운영되고 있으면, Nginx를 reload 하는 코드를 작성합니다. 또한 Nginx가 각 컨테이너를 잘 찾을 수 있도록 `upstream`에 각 컨테이너의 IP를 넣도록 합니다.

#!/bin/bash

get_new_django_container_IPs() {
    # 각 컨테이너의 ID 조회
    id1=$(docker container ls --all --quiet --filter "name=project_${1}_1")
    id2=$(docker container ls --all --quiet --filter "name=project_${1}_2")

    # 각 컨테이너의 IP 조회
    ip1=$(docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" ${id1})
    ip2=$(docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" ${id2})

    # IP를 담은 배열
    ip_arr=($ip1 $ip2)

    # 배열 반환
    echo ${ip_arr[@]}
}

reload_nginx_container() {
    cp ./nginx/nginx.$1.conf ./nginx/nginx.conf

    server=$1
    server+="_server1"
    sed -i "s/$server/$2/g" ./nginx/nginx.conf  # blue/green_server_1 문자열을 $2값으로 치환

    server=$1
    server+="_server2"
    sed -i "s/$server/$3/g" ./nginx/nginx.conf  # blue/green_server_2 문자열을 $3값으로 치환

    docker exec project_nginx service nginx reload
    echo "container project_nginx reload..."
}

down_django_container() {
    docker compose -p project-$1 -f docker-compose.$1.yml down
    echo "container $1 down..."
}

# Blue를 기준으로 현재 실행 중인 컨테이너 체크
EXIST_BLUE=$(docker ps | grep project_blue)

# 컨테이너 스위칭
if [ -z "$EXIST_BLUE" ]; then
    echo "Deploy Blue..."
    sudo docker compose -p project-blue -f docker-compose.blue.yml up --build -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"

    sleep 2m 30s

    # 새로운 컨테이너가 정상적으로 운영 중인지 확인
    EXIST_AFTER=$(docker compose -p project-${AFTER_COMPOSE_COLOR} -f docker-compose.${AFTER_COMPOSE_COLOR}.yml ps | grep Up)
    if [ -n "$EXIST_AFTER" ]; then

        # nginx 컨테이너 실행
        sudo docker compose -p project-nginx -f docker-compose.nginx.yml up --build -d
        sleep 10s

        # 새로운 두 컨테이너의 IP 반환 (IPS: 배열)
        ip_arr=$(get_new_django_container_IPs $AFTER_COMPOSE_COLOR)

        # nginx.conf를 컨테이너에 맞게 변경한 후 reload
        reload_nginx_container $AFTER_COMPOSE_COLOR ${ip_arr[0]} ${ip_arr[1]}

        # 정상적으로 운영 중이라면 이전 컨테이너 종료
        down_django_container $BEFORE_COMPOSE_COLOR
    fi

else
    echo "Deploy Green..."
    sudo docker compose -p project-green -f docker-compose.green.yml up --build -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"

    sleep 2m 30s

    # 새로운 컨테이너가 정상적으로 운영 중인지 확인
    EXIST_AFTER=$(docker compose -p project-${AFTER_COMPOSE_COLOR} -f docker-compose.${AFTER_COMPOSE_COLOR}.yml ps | grep Up)
    if [ -n "$EXIST_AFTER" ]; then

        # nginx 컨테이너 실행
        sudo docker compose -p project-nginx -f docker-compose.nginx.yml up --build -d
        sleep 10s

        # 새로운 두 컨테이너의 IP 반환 (IPS: 배열)
        ip_arr=$(get_new_django_container_IPs $AFTER_COMPOSE_COLOR)

        # nginx.conf를 컨테이너에 맞게 변경한 후 reload
        reload_nginx_container $AFTER_COMPOSE_COLOR ${ip_arr[0]} ${ip_arr[1]}

        # 정상적으로 운영 중이라면 이전 컨테이너 종료
        down_django_container $BEFORE_COMPOSE_COLOR
    fi
fi

 

 

결과

작성한 쉘 스크립트대로 배포를 진행할 수 있었으나, Nginx가 각 컨테이너로 연결을 하지 못하는 현상 발생. 원인으로 고려해 볼 만한 사항은 서버 환경이 DNS라는 점. 따라서 DNS 환경에서 Blue/Green 배포를 할 수 있는 방법을 찾아봐야 함.

또한 쉘 스크립트대로 잘 진행은 됐으나, 어찌 됐건 중단이 발생한다는 점에서 무중단 배포는 아님. 

 

Blue/Green Deployment with DNS

DNS 환경에서 Blue/Green 배포를 하려면 docker compose 파일에 네트워크 설정을 추가적으로 해야 하는 것 같습니다. 또한 기존 docker compose 파일에도 수정이 필요할 것으로 보입니다.

사실 지금 작성한 버전으로는 배포 과정에서 중단이 발생하기 때문에 무중단 배포라고 하기 어려운 면이 있었습니다. 따라서 위와 같은 깨달음을 바탕으로 추후 다시 시도해 보는 것으로 하고, blue/green 배포는 여기서 잠시 마무리합니다 🥲

 

 

 

 

 

참고

 

'Architecture & Tool' 카테고리의 다른 글

윈도우에 프로메테우스 설치하기  (0) 2024.05.03
롤링 무중단 배포  (0) 2024.04.19
서버 부하 테스트 with Locust  (0) 2023.12.16
실시간 모니터링 도구 - Glances  (0) 2023.05.29
Windows 10에 Tomcat 8 설치  (0) 2022.01.08
Comments