3. CI/CD 구축하기 - Nginx를 활용한 블루/그린 배포 방식

2023. 4. 7. 10:05초기 과업/BackEnd

작성자알 수 없는 사용자

728x90
반응형

 

안녕하세요. 기깔나는 사람들에서 백앤드를 맡고있는 Hardy입니다.

 

 

이번에는 저번 챕터에 이어서 nginx 설치 및 블루/그린 배포 방식을 진행하려고 합니다.

 

nginx는 도커 컨테이너로 구동하고 있으면 배포가 진행될 인스턴스2개가 필요합니다.

 

시작해 보겠습니다.

 

 

 

 

 

 

 

 

 


Nginx 및 서버가 배포될 2개 인스턴스 생성하기

 

 

먼저 아래와 같이 EC2에서 Nginx , 배포될 인스턴스를 2개생성해 주시면 됩니다.

 

이렇게 3가지의 인스턴스가 생성이 되었다면 3개의 인스턴스에 도커를 설치해주시면 됩니다.

 

아래 글에 가시면 인스턴스 및 도커 설치 방법이 나와 있어요!!

2023.04.07 - [분류 전체보기] - Jenkins 를 활용한 CI/CD구축하기

 

 

여기서  중요한 부분이 있어요

 

Nginx는 로드밸런싱 + 리버스 프록시 역할을 할꺼에요!!

 

이말은 즉 모든 요청을 Nginx에서 받아서 appServer1 , app_server_22서버로 요청을 다시 로드밸런싱 한다고 생각하시면 됩니다.

 

그렇기에 직접적으로 서버가 돌아갈 appServer1 , app_server_22인스턴스는 Nginx로 부터 들어오는 요청만 처리 할 수 있도록 보안 그룹을 설정해주셔야 해요!!

 

 

 

 

 

보안그룹

 

현재 52.79.99.80은 Nginx가 구동될 서버의 주소에요 !! 해당 주소로 부터 들어오는 5001 , 5000 포트만 인바운드 되도로록 appServer1 , app_server_22에 모두 적용해 주시면 됩니다.

 

 

 

 

 

보안 그룹 생성

보안 그룹 수정은 원하시는 인스턴스 선택 > 보안 > 보안그룹 아이디를 누르시면 수정이 가능합니다.

 

 

 

 

 


 

 

Nginx및 서버 배포 방식(blue / green)

 

우선 blue/green배포 방식에 대해서 간단하게 설명 드리겠습니다.

 

서버가 배포가 되서 실행이 되면 최초 blue존에서 서버가 실행되고 nginx가 blue존을 바라보며 로드밸런싱을 시작합니다.

그 이후 서버가 수정이되어서 새로운 버전이 나오게 되면 green존에 새로운 배포 버전을 실행하고 nginx설정을 green존을 바라 보도록 변경을 합니다. 그 후 정상적으로 되었다면 blue존에 있는 서버를 중지 시킵니다

해당 방식을 지속적으로 반복해 나가면서 배포하는 방식을 blue/green이라고 합니다.

 

 

서버 배포 방식에 대해 먼저 설명 드리겠습니다.

 

1. Jenkins에서 Nginx로 파일 전달

 app/docker-compose.yml , nginx/conf.d/docker-compose.yml,nginx.conf, nginx-blue.conf, nginx-green.conf , back_server.tar(빌드된 jar파일을 도커 이미지로 만든것) ,deploy.sh 해당 파일들을 1번을 통해 nginx서버로 전송이 됩니다.

 

2. nginx는 deploy.sh 스크립트를 통해서 app서버에 app/docker-compose.yml , back_server.tar(빌드된 jar파일을 도커 이미지로 만든것) 을 각각 전송하고 실행합니다.

 

3. 그 이후에는 켜진 서버가 blue존인지 green존인지에 따라서 nginx.conf파일을 변경한 후에 재시작 합니다.

 

간단한 배포 플로우 입니다. 자세한건 밑에 계속 설명해 나가겠습니다

 

 

 

 


 

 

Jenkins에서 Nginx로 파일을 보내기 위한 Publish Over SSH 설치

 

plugin 설치

jenkinsfile에서 원격서버로 파일을 보내고 , command를 실행 할 수 있는 Publish Over SSH 플러그인을 설치해줍니다.

 

해당 플러그인을 설치한 후

 

키 등록

 

DashBoard > jenkins > system에 들어가서 비밀키 경로와 비밀키를 등록해줍니다.

아마 기본적으로 비밀키가 해당 경로에 없을꺼에요!

 

지금 jenkins가 설치된 서버에 ssh 접속을 해봅시다

docker로 jenkins가 돌고 있을꺼에요! 그럼 docker exec -it {컨테이너이름} bash로 컨테이너에 들어 갑시다.

 

그 이후 해당 명령어로 키를 생성해 줍시다

ssh-keygen -t ecdsa -b 521 -m PEM

 

생성이 완료 되면 /var/jenkins_home/.ssh 파일 경로에 키가 생길꺼에요!

 

키값

그 이후에 id_ecdsa.pub에 있는 값을 복사 한 후 nginx서버에 접속 합니다.

 

 

 

키등록

id_ecdsa.pub에 있는 값authorized_keys에 복사를 합니다.

 

이렇게 되면 jenkins에서 ssh로 nginx에 접근 할 수 있어요!

 

 

 

 

젠킨스로 돌아와서 ssh Server로 nginx서버 설정을 해줍시다

중요한건 hostname , username(nginx 사용자명), remote directory(원격 서버에 root가 될 path) 를 확실하게 설정해주세요!

 

다 입력 후 Test Configuration을 클릭하면 제대로 설정이 되어 있다면 success가 뜰꺼에요!

 

여기까지 완료 되었다면 jenkins와 nginx간 통신 설정이 완료 되었습니다

 

 

여기서 잠깐!!!!!!

위에 배포 방식을 보시면 nginx에서도 app 서버로 ssh통신을 통해 파일을 전송한다고 말씀드렸습니다.

즉 ssh설정을 똑같이 nginx와 app에도 해주셔야합니다.

 

ssh설정이란 키를 만들고 authorized_keys에 등록 하는걸 말합니다!!

 

즉 nginx에서 키를 만들고 해당 키의 공개키를 각각 app인스턴스에 authorized_keys에 등록 하신 후 접속 을 한번 해봐주세요

 


 

 

Jenkins에서 Nginx로 보낼 파일 항목

 

이제 Jenkins에서 Nginx로 보낼 파일들을 설명하도록 하겠습니다.

 

1. nginx/conf.d 디렉토리 및 하위 파일들

nginx파일

 

 

해당 파일들은 빌드될 프로젝트에 존재하고 있답니다.

 

docker-compose.yml

version: '3.3'
services:
  nginx:
    image: nginx
    ports:
      - '80:80'
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - /home/ubuntu/nginx/conf.d/:/etc/nginx/
    container_name: nginx_server

해당 파일은 nginx를 컨테이너를 실행시킬 compose 파일 입니다.

봐주실꺼는 volumes 입니다. blue/green 배포시 계속 nginx.conf파일이 변경 되기 때문에 해당 폴더를 volume으로 로컬경로와 매칭 시켜줬습니다.

 

 

nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 3.38.238.0:5000;
        server 3.38.238.0:5001;
    }

    server {
        listen 80;
        server_name proxy;

        location / {
            add_header 'Access-Control-Allow-Origin' '*' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
            add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token' always;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

기본적으로 nginx에 volume을 설정하면 default nginx.conf가 필요합니다.

 

upstream backend 블럭은 nginx가 요청을 받으면 upstream backend에 적힌 곳으로 요청을 보내겠다라는 의미를 가진 block입니다.

 

server 에는 프록시에 대한 설정이 되어 있습니다. 

 

proxy_set_header Host $host; //요청에 대한 호스트 헤더를 설정
proxy_set_header X-Real-IP $remote_addr; 클라이언트의 실제 IP 주소를 X-Real-IP라는 새로운 헤더로 설정
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; // 클라이언트의 IP 주소를 추적을 우한 헤더 설정

nginx-blue.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 3.38.238.0:5000;
        server 3.38.238.0:5001;
    }

    server {
        listen 80;
        server_name proxy;

        location / {
            add_header 'Access-Control-Allow-Origin' '*' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
            add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token' always;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

 

nginx-green.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 3.38.126.146:5000;
        server 3.38.126.146:5001;
    }

    server {
        listen 80;
        server_name proxy;

        location / {
            add_header 'Access-Control-Allow-Origin' '*' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
            add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token' always;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

 

blue와 green .conf의 차이점은 upstream backend에 설정된 ip 주소 입니다.

 

 

2.  app/docker-compose.yml 디렉토리 및 하위 파일들

docker-compose.yml

 

 

 

docker-compose.yml

version: '3.3'

services:
  db:
    image: mariadb
    environment:
      MYSQL_DATABASE: giga
      MYSQL_USER: seungki1993
      MYSQL_PASSWORD: dltmdrl123!
      MYSQL_ROOT_PASSWORD: dltmdrl123!
    ports:
      - "3306:3306"
    volumes:
      - /home/ubuntu/data:/var/lib/mysql

  server-1:
    image: back_server:latest
    ports:
      - "5000:8080"
    depends_on:
      - db
    container_name: server_1

  server-2:
    image: back_server:latest
    ports:
      - "5001:8080"
    container_name: server_2
    depends_on:
      - db

해당 docker-compose 파일은 app 인스턴스에 있는 서버 이미지를 실행시키는 파일입니다.

 

서버는 DB도 필요로 하기 때문에 DB 서비스도 같이 올리도록 하였습니다.

 

depends_on은 DB가 정상적으로 구동된 상태에서 실행하겠다는 의미입니다.

 

 

 

 

3.  deploy.sh 

deploy.sh

 

#!/bin/bash

// 1
NGINX_DIR=/home/ubuntu/nginx/conf.d
BLUE=3.38.238.0
GREEN=3.38.126.146
CURRENT=blue

//2
scp /home/ubuntu/app/docker-compose.yml ubuntu@3.38.238.0:/home/ubuntu/docker-compose.yml
scp /home/ubuntu/back_server.tar ubuntu@3.38.238.0:/home/ubuntu/back_server.tar
count=0
while [ -z "$(ssh ubuntu@${BLUE} "ls -al | grep back_server.tar")" ]
do
  echo "sending"

  if [ $count -eq 5 ]; then
    echo "tar not send : "+count
  fi
  count=$((count+1))
done

//3
scp /home/ubuntu/app/docker-compose.yml ubuntu@3.38.126.146:/home/ubuntu/docker-compose.yml
scp /home/ubuntu/back_server.tar ubuntu@3.38.126.146:/home/ubuntu/back_server.tar
count=0
while [ -z "$(ssh ubuntu@${GREEN} "ls -al | grep back_server.tar")" ]
do
  echo "sending"

  if [ $count -eq 5 ]; then
    echo "tar not send : "+count
      exit 1
  fi
  count=$((count+1))
  sleep 4
done

//4
ssh ubuntu@3.38.238.0 "cd /home/ubuntu/ && docker load --input back_server.tar"
ssh ubuntu@3.38.126.146 "cd /home/ubuntu/ && docker load --input back_server.tar"

//5
EXIST_NGINX=$(docker ps | grep nginx_server)
check=0
if [ -z "$EXIST_NGINX" ]; then
    echo "nginx container start"
    docker-compose -f ${NGINX_DIR}/docker-compose.yml up -d
    while [ EXIST_NGINX=$(docker ps | grep nginx_server) ]; do
        if [ $check -eq 5 ]; then
            echo "nginx run failed"
            exit 1
        fi
        check=$((check+1))
        sleep 4
    done
else
    echo "nginx is already running"
fi

sleep 10

//6
EXIST_BLUE_1=$(ssh ubuntu@${BLUE} "docker ps | grep server_1")
EXIST_BLUE_2=$(ssh ubuntu@${BLUE} "docker ps | grep server_2")
checkServer=0
if [[ -z "$EXIST_BLUE_1" && -z "$EXIST_BLUE_2" ]]; then
        ssh ubuntu@${BLUE} "cd /home/ubuntu && docker-compose up -d"
        sleep 10
        while [EXIST_BLUE_1=$(ssh ubuntu@${BLUE} "docker ps | grep server_1") ]; do
                if [ checkServer -eq 5 ]; then
                    echo "blue run failed"
                    exit 1
                fi
                checkServer=$((checkServer+1))
                sleep 4
            done
else
        ssh ubuntu@${GREEN} "cd /home/ubuntu && docker-compose up -d"
        sleep 10
         while [EXIST_GREEN_1=$(ssh ubuntu@${GREEN} "docker ps | grep server_1") ]; do
                        if [ checkServer -eq 5 ]; then
                            echo "blue run failed"
                            exit 1
                        fi
                        checkServer=$((checkServer+1))
                        sleep 4
                    done
        CURRENT=green
fi

//7
if [ "$CURRENT" == "green" ]; then
    ssh ubuntu@${BLUE} "docker-compose down"
else
    echo $CURRENT
    ssh ubuntu@${GREEN} "docker-compose down"
fi
cp ${NGINX_DIR}/nginx-${CURRENT}.conf ${NGINX_DIR}/nginx.conf
docker exec nginx_server nginx -s reload
exit 0

 

1.

NGINX_DIR=/home/ubuntu/nginx/conf.d // 현재 nginx서버에 nginx경로를 NGINX_DIR변수에 할당
BLUE=3.38.238.0 // BLUE서버의 ip 주소를 할당
GREEN=3.38.126.146 // GREEN서버의 ip 주소를 할당
CURRENT=blue // 맨처음 구동시에는 Blue에서 먼저 실행되어 하기 때문에 Blue로 변수 할당

 

2.

//2 
//2.1
scp /home/ubuntu/app/docker-compose.yml ubuntu@3.38.238.0:/home/ubuntu/docker-compose.yml
//2.2
scp /home/ubuntu/back_server.tar ubuntu@3.38.238.0:/home/ubuntu/back_server.tar
//2.3
count=0
while [ -z "$(ssh ubuntu@${BLUE} "ls -al | grep back_server.tar")" ]
do
  echo "sending"

  if [ $count -eq 5 ]; then
    echo "tar not send : "+count
  fi
  count=$((count+1))
done

 

2.1  현재 nginx서버 경로에 /app/docker-compose.yml 파일을 원격 서버 3.38.238.0로 파일 전송

2.2  현재 nginx서버 경로에 /back_server.tar(이미지파일) 을 원격 서버 3.38.238.0로 파일 전송

2.3  while문을 돌면서 3.38.238.0서버에 back_server.tar있는지 검증

        검증은 최대 5번을 수행하고 5번이 된다면 sh 종료

 

 

3.

//3
// 3.1
scp /home/ubuntu/app/docker-compose.yml ubuntu@3.38.126.146:/home/ubuntu/docker-compose.yml
// 3.2
scp /home/ubuntu/back_server.tar ubuntu@3.38.126.146:/home/ubuntu/back_server.tar
// 3.3
count=0
while [ -z "$(ssh ubuntu@${GREEN} "ls -al | grep back_server.tar")" ]
do
  echo "sending"

  if [ $count -eq 5 ]; then
    echo "tar not send : "+count
      exit 1
  fi
  count=$((count+1))
  sleep 4
done

 

3.1  현재 nginx서버 경로에 /app/docker-compose.yml 파일을 원격 서버 3.38.126.146로 파일 전송

3.2  현재 nginx서버 경로에 /back_server.tar(이미지파일) 을 원격 서버 3.38.126.146로 파일 전송

3.3  while문을 돌면서 3.38.126.146서버에 back_server.tar있는지 검증

        검증은 최대 5번을 수행하고 5번이 된다면 sh 종료

 

 

4.

//4
ssh ubuntu@3.38.238.0 "cd /home/ubuntu/ && docker load --input back_server.tar"
ssh ubuntu@3.38.126.146 "cd /home/ubuntu/ && docker load --input back_server.tar"

 

각각 서버에 보내진 back_server.tar 파일을 docker 명령어를 통해 tar파일에 있는 이미지를 로드 시킴

즉 해당 서버에 이미지로 등록이 됩니다.

 

 

5.

//5
EXIST_NGINX=$(docker ps | grep nginx_server)
check=0
if [ -z "$EXIST_NGINX" ]; then
    echo "nginx container start"
    docker-compose -f ${NGINX_DIR}/docker-compose.yml up -d
    while [ EXIST_NGINX=$(docker ps | grep nginx_server) ]; do
        if [ $check -eq 5 ]; then
            echo "nginx run failed"
            exit 1
        fi
        check=$((check+1))
        sleep 4
    done
else
    echo "nginx is already running"
fi

sleep 10

 

 NGINX가 현재 실행중인지 docker ps | grep nginx_server명령어로 확인합니다.

그 이후 Nginx가 실행중이지 않으면 docker-compose파일로 nginx를 실행시킵니다.

이때 실행되는건 기본파일인 nginx.conf파일로 실행이 됩니다. 

 

만약 Nginx가 구동중이라면 다음 스텝으로 넘어 갑니다.

 

 

 

6.

//6
EXIST_BLUE_1=$(ssh ubuntu@${BLUE} "docker ps | grep server_1")
EXIST_BLUE_2=$(ssh ubuntu@${BLUE} "docker ps | grep server_2")
checkServer=0
if [[ -z "$EXIST_BLUE_1" && -z "$EXIST_BLUE_2" ]]; then
        ssh ubuntu@${BLUE} "cd /home/ubuntu && docker-compose up -d"
        sleep 10
        while [EXIST_BLUE_1=$(ssh ubuntu@${BLUE} "docker ps | grep server_1") ]; do
                if [ checkServer -eq 5 ]; then
                    echo "blue run failed"
                    exit 1
                fi
                checkServer=$((checkServer+1))
                sleep 4
            done
else
        ssh ubuntu@${GREEN} "cd /home/ubuntu && docker-compose up -d"
        sleep 10
         while [EXIST_GREEN_1=$(ssh ubuntu@${GREEN} "docker ps | grep server_1") ]; do
                        if [ checkServer -eq 5 ]; then
                            echo "blue run failed"
                            exit 1
                        fi
                        checkServer=$((checkServer+1))
                        sleep 4
                    done
        CURRENT=green
fi

 

Blue서버에 server_1 , server_2라는 컨테이너가 구동중인지 확인을 합니다.

만약 구동중이지 않다면 Blue서버에 docker-compose파일을 통해 서버를 구동합니다.

그 이후 반복문을 돌면서 server_1이 구동중인지 확인을 합니다.

만약 5번의 반복문을 돌때까지 켜지지 않는다면 서버에 문제가 있다고 판단하여 sh종료 시킵니다.

 

만약 Blue서버에 server_1과 server_2가 구동중이라면 Green서버에 docker-compose로 컨테이너를 구동 시킵니다.

해당 Green서버도 마찬가지로 반복문을 돌면서 켜지는것을 확인하고 5번에 걸쳐도 켜지지 않는다면 

sh을 종료 시킵니다. 

그 이후 만약 Green존에 컨테이너가 켜진다면 Current를 Green으로 변경합니다.

 

 

 

7.

//7
if [ "$CURRENT" == "green" ]; then
    ssh ubuntu@${BLUE} "docker-compose down"
else
    echo $CURRENT
    ssh ubuntu@${GREEN} "docker-compose down"
fi
cp ${NGINX_DIR}/nginx-${CURRENT}.conf ${NGINX_DIR}/nginx.conf
docker exec nginx_server nginx -s reload
exit 0

 

현재 Current가 Green이라면 Blue서버에 컨테이너를 docker-compose로 중지 시킵니다.

그와 반대로 Blue라면 Grenn서버에 컨테이너를 docker-compose로 중지 시킵니다

 

그 이후 Nginx를 현재 실행중인 서버에 맞게 conf파일을 변경 한 후 reload시킵니다.

 

이런한 과정으로 서버는 Blue/Green방식으로 배포가 완료 됩니다.

 

 

 


JenkinsFile 수정

 

stage('docker build') {
            steps {
                script {
                    def image = docker.build("back_server:latest")
                    sh "docker save ${image.id} -o back_server.tar"


                }
            }
        }

해당 stage는 빌드된 jar파일을 도커 이미지로 빌드를 하게 됩니다.

현재는 도커 허브를 사용하고 있지 않기 때문에 docker image를 tar 형식을 만들어 저장합니다.

여기서 주의할점이 있습니다.

도커 이미지를 생성하기 위해서는 dockerfile이 필요로 합니다.

 

FROM openjdk:17
COPY /app.jar app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=local","/app.jar"]

 

해당 문법이 있는 dockerfile을 생성하여 아래 경로에 넣어주시면 됩니다!!!!

dockerfile 위치

 

 

 

 

 stage('deploy') {
            steps([$class: 'BapSshPromotionPublisherPlugin']) {
                script {
                    sshPublisher(
                            continueOnError: false, failOnError: true,
                            publishers: [
                                    sshPublisherDesc(
                                            configName: "nginx",
                                            verbose: true,
                                            transfers: [
                                                    sshTransfer(sourceFiles: "nginx/**"),
                                                    sshTransfer(sourceFiles: "app/**"),
                                                    sshTransfer(sourceFiles: "back_server.tar"),
                                                    sshTransfer(sourceFiles: "deploy.sh", execCommand: "chmod +x deploy.sh && ./deploy.sh")
                                            ]
                                    )
                            ]
                    )
                }
            }
        }

 

해당 step은 Publish over SSh 플러그인을 사용하여 Nginx서버에 파일들을 전송하고 실행합니다.

 

sshPublisherDesc에 transfers를 보시겠습니다.

 

sshTransfer의 sourceFiles 구문은 해당 파일을 원격 서버에 보내겠다는 의미입니다.

 

sshTransfer의 execCommand 구문은 어떠한 명령어를 원격서버에서 실행할지를 의미합니다.

 

 

여기까지 완료가 되었다면 모든 준비는 끝이 났습니다. 한번 push를 보내고 Jenkins를 확인해볼까요??

 

 

맨처음 시동이니 당연히블루 서버에 컨테이너가 실행이 되어야겠죠??

 

blue 서버존

 

nginx.conf

 

두이미지를 보면 blue를 바라보게 되어 있고 blue서버에 잘 구동이 되어 있네요!!

 

 

다시한번 파이프라인을 돌려볼께요!!

 

green 존

 

nginx.conf

 

둘다 green서버를 바라보게 바뀌었네요!!!

 

 

여기까지 jenkins + sonarqube + nginx  를 활용하여 블루 / 그린 배포를 완료 했습니다.

 

마지막 다음 챕터는 ansbile을 사용하여 도커를 구성하고 , 블루 / 그린 배포를 하도록 하겠습니다.

 

 

 

 


 

 

 

 

 

728x90
반응형