프로젝트/뉴스타

[Infra] Blue-Green 배포 전략을 활용한 무중단 서비스

cks._.hong 2024. 8. 9. 12:40

Blue-Green 배포 전략을 활용한 무중단 서비스 왜 궁금했을까❓

뉴스타 프로젝트에 Blue-Green 배포 전략을 도입하여 무중단 서비스를 구축한 과정에 대해 작성해보려고 한다. 또한, 배포 전략 중에 Blue-Green을 선택한 이유도 작성해보겠다.

 

 

[Infra] 무중단 배포 전략 (Rolling / Blue-Green / Canary)

무중단 배포 전략 (Rolling / Blue-Green / Canary) 왜 궁금했을까❓뉴스타 서비스 CI/CD 과정에서 서비스가 중단되는 문제점으로 인해 사용자가 일정 시간동안 서비스를 이용하지 못하는 문제점이 있었

pslog.co.kr

위 포스팅을 통해 무중단 배포 전략에 대한 개념을 학습할 수 있다.

1. 왜 Blue-Green 배포 전략인가?

  • 1개의 서버와 어플리케이션당 하나씩 Docker를 이용해서 띄우기 때문에 롤링과 카나리 방식은 적합하지 않았다.
  • 프로젝트 기간이 짧아 기능의 품질을 보장하기 쉽지 않았고 이로 인해 잦은 백업이 필요했다. 보다 쉽게 롤백을 이뤄낼 수 있기 때문에 Blue-Green을 선택했다.
  • Blue-Green의 경우 리소스가 많이 사용된다는 점이 있지만 어플리케이션을 3개 밖에 구동중이지 않아 전환되는 과정에서 부하가 많지 않을 것이라 예상되었다.

2. 프로젝트 구축 과정

2.1. Blue-Green 무중단 서비스 Flow

  1. 개발자가 코드 변경 사항을 GitLab에 PUSH
  2. Webhook을 통해서 신버전 코드 빌드 및 이미지 배포
  3. Blue-Green 전환 배포 스크립트 Deploy.sh 실행
  4. Nginx 라우팅을 통해서 구버전 -> 신버전 전환
 

[CI/CD] Jenkins / GitLab / Docker / EC2 연동 (1/2)

SSAFY 2학기 프로젝트 "Archiview"를 Jenkins, GitLab, Docker, EC2를 이용해서 CI/CD를 구축해보겠다.처음에는 Jenkins를 Docker에 올렸는데 Jenkins에서 docker를 실행하기 위해서는 docker를 또 설치해야 하는 문제점

pslog.co.kr

위 포스팅을 통해서 Jenkins, GitLab을 활용한 CI/CD 과정을 볼 수 있다.

2.2. Nginx

service-url.inc

set $spring_url http://172.17.0.1:8080;
set $react_url http://172.17.0.1:3000;
set $fastapi_url http://172.17.0.1:8000;
  • 서버 IP를 각 변수에 저장하고 해당 IP를 변경하여 Blue-Green 전환이 이뤄지게 된다.

default.conf

server {
    ...
    location /api {
        proxy_pass $spring_url;
        proxy_set_header X-Forwarded-Host $server_name;
    }

    location /api/data/ {
        proxy_pass $fastapi_url;
        proxy_set_header X-Forwarded-Host $server_name;
    }

    location / {
        proxy_pass $react_url;
    }
}
  • proxy_pass에 각 서버에 해당되는 변수를 입력하여 서버 IP를 지정하여 라우팅한다.

2.3. Docker

docker-compose.blue.yaml

version: '3'

services:
    spring:
        container_name: spring-blue
        image: newstar_back
        ports:
          - 8080:8080
        restart: always
        env_file:
          - /env/.env
        environment:
          TZ: Asia/Seoul
    react:
        container_name: react-blue
        image: newstar_front
        ports:
          - 3000:3000
    fastapi:
        container_name: fastapi-blue
        image: fastapi_back
        ports:
          - 8000:8000
        restart: always
        env_file:
          - /env/.env
        environment:
          TZ: Asia/Seoul
networks:
  default:
    name: app-net
    external: true

docker-compose.green.yaml

version: '3'

services:
    spring:
        container_name: spring-green
        image: newstar_back
        ports:
          - 8081:8080
        restart: always
        env_file:
          - /env/.env
        environment:
          TZ: Asia/Seoul

    react:
        container_name: react-green
        image: newstar_front
        ports:
          - 3001:3000
    fastapi:
        container_name: fastapi-green
        image: fastapi_back
        ports:
          - 8001:8000
        restart: always
        env_file:
          - /env/.env
        environment:
          TZ: Asia/Seoul
networks:
  default:
    name: app-net
    external: true

2.4. Jenkins Pipeline Script

pipeline {
    agent any
    
    stages {
        stage('clone') {
            steps {
                echo 'git clone'
                git branch: 'master', 
                credentialsId: '[비밀키]', 
                url: 'https://lab.ssafy.com/s10-bigdata-recom-sub2/S10P22B302.git'
            }
        }
        stage('API Server build') {
            steps {
                dir('back') {
                    sh 'docker build -t newstar_back .'
                }
            }
        }
        stage('GPU Server build') {
            steps {
                dir('pydata') {
                    sh 'docker build -t fastapi_back .'
                }
            }
        }
        stage('Web Server build') {
            steps {
                dir('front/newstar') {
                    sh 'docker build -t newstar_front .'
                }
            }
        }
        stage('Blue-Green Deploy'){
            steps{
                dir('exec/deploy'){
                    sh 'chmod +x deploy.sh'
                    sh './deploy.sh'
                }
            }
        }
    }
}
  1. WebHook에 의해 파이프라인이 실행되는데 GitLab에 있는 Repo로부터 코드를 Clone 받아온다.
  2. API, GPU, Web Server를 순으로 신버전의 도커 이미지를 빌드한다.
  3. deploy.sh를 통해 Blue-Green 배포를 실시한다.

2.5. deploy.sh

#!/bin/bash

echo 'CI/CD Deploy Start'

cd ../exec/deploy
# Working container check
EXIST_BLUE=$(docker compose -p deploy-blue -f docker-compose.blue.yaml ps | grep Up)

if [ -z "$EXIST_BLUE" ]; then
    # blue
    docker compose -p deploy-blue -f docker-compose.blue.yaml up -d
    BEFORE_COLOR="green"
    AFTER_COLOR="blue"
    BEFORE_SPRING_PORT=8081
    BEFORE_REACT_PORT=3001
    BEFORE_FASTAPI_PORT=8001
    AFTER_SPRING_PORT=8080
    AFTER_REACT_PORT=3000
    AFTER_FASTAPI_PORT=8000
else
    # green
    docker compose -p deploy-green -f docker-compose.green.yaml up -d
    BEFORE_COLOR="blue"
    AFTER_COLOR="green"
    BEFORE_SPRING_PORT=8080
    BEFORE_REACT_PORT=3000
    BEFORE_FASTAPI_PORT=8000
    AFTER_SPRING_PORT=8081
    AFTER_REACT_PORT=3001
    AFTER_FASTAPI_PORT=8001
fi

# Spring Server health checking
for retry_count in {1..60}
do
    response=$(curl -s http://172.17.0.1:${AFTER_SPRING_PORT}/api/actuator/health)
    up_count=$(echo $response | grep 'UP' | wc -l)

    if [ $up_count -ge 1 ]
    then
        echo "=========================="
        echo "> Spring Server is working"
        echo "=========================="
        break
    else
        echo "> Spring Health is not working: ${response}"
    fi
    # about 10 minuetes
    if [ $retry_count -eq 60 ]
    then
        echo "> Spring Server working failed"
        docker compose -p deploy-${AFTER_COLOR} -f docker-compose.${AFTER_COLOR}.yaml down
        exit 1;
    fi
    # wating 10 seconds
    sleep 10
done

# Fastapi Server health checking
for retry_count in {1..60}
do
    response=$(curl -s http://172.17.0.1:${AFTER_FASTAPI_PORT}/api/data/health)
    up_count=$(echo $response | grep 'UP' | wc -l)

    if [ $up_count -ge 1 ]
    then
        echo "=========================="
        echo "> Fastapi Server is working"
        echo "=========================="
        break
    else
        echo "> Fastapi Health is not working: ${response}"
    fi
    # about 10 minuetes
    if [ $retry_count -eq 60 ]
    then
        echo "> Fastapi Server working failed"
        docker compose -p deploy-${AFTER_COLOR} -f docker-compose.${AFTER_COLOR}.yaml down
        exit 1;
    fi
    # wating 10 seconds
    sleep 20
done

echo "${AFTER_COLOR} server up(spring_port:${AFTER_SPRING_PORT}, react_port:${AFTER_REACT_PORT}, fastapi_port:${AFTER_FASTAPI_PORT})"

EXIST_AFTER=$(docker compose -p deploy-${AFTER_COLOR} -f docker-compose.${AFTER_COLOR}.yaml ps | grep Up)

if [ -n "$EXIST_AFTER" ]; then
    echo "nginx Setting"
    docker exec -i nginx /bin/bash -c "echo -e 'set \$spring_url http://172.17.0.1:${AFTER_SPRING_PORT};\nset \$react_url http://172.17.0.1:${AFTER_REACT_PORT};\nset \$fastapi_url http://172.17.0.1:${AFTER_FASTAPI_PORT};' | tee /etc/nginx/conf.d/service-url.inc && nginx -s reload"
    
    echo "Completed Deploy!"
    echo "$BEFORE_COLOR server down(spring_port:${BEFORE_SPRING_PORT}, react_port:${BEFORE_REACT_PORT}, fastapi_port:${BEFORE_FASTAPI_PORT})"
    docker compose -p deploy-${BEFORE_COLOR} -f docker-compose.${BEFORE_COLOR}.yaml down
fi
  1. 현재 실행중인 Docker-compose의 색깔을 확인(Blue or Green)
  2. 만약, Blue일 경우 Green의 Docker-compose를 띄우고 port번호를 Green에 맞게 저장
  3. Green일 경우 Blue의 Docker-compose를 띄우고 port번호를 Blue에 맞게 저장
  4. 각 어플리케이션 API, GPU Server의 Health Check API를 통해 요청을 날려보며 신버전의 코드가 정상적으로 운영되고 있는지 확인
  5. 만약, 정상적으로 운영되지 않으면 신버전의 서버 다운 후 스크립트 종료
  6. 신버전의 Docker-compose가 정상적으로 띄워져 있다면 service-url.inc에 있는 각 어플리케이션 port를 신버전으로 변경 후 Nginx 재시작
  7. 기존에 운영되던 구버전의 docker compose 다운