hye-log

Nginx-SpringBoot 무중단 배포 도입기 - (3) deploy.sh/switch.sh 본문

Infra/Nginx

Nginx-SpringBoot 무중단 배포 도입기 - (3) deploy.sh/switch.sh

iihye_ 2024. 4. 14. 03:26

지금까지 설정만 다 했으면

여기는 파일만 잘 쓰면 끝이다!

 

1. Dockerfile

FROM openjdk:17
ARG IDLE_PROFILE
ENV ENV_IDLE_PROFILE=$IDLE_PROFILE
COPY build/libs/{{.jar}} /app/app.jar
RUN echo $ENV_IDLE_PROFILE
ENTRYPOINT ["java", "-Dspring.profiles.active=${ENV_IDLE_PROFILE}", "-jar", "/app/app.jar"]

Dockerfile부터 작성해준다

IDEL_PROFILE은 deploy.sh에서 설정해줄텐데

새로운 profile로 실행하도록 설정해준다

 

2. application.yml

spring:
  profiles:
    active: prod1
    group:
      prod1: common, prod1-server
      prod2: common, prod2-server

server:
  env: blue
---
spring:
  config:
    activate:
      on-profile: common

serverName: common
---
spring:
  config:
    activate:
      on-profile: prod1-server
serverInfo: prod1
server:
  port: 9091
---
spring:
  config:
    activate:
      on-profile: prod2-server
serverInfo: prod2
server:
  port: 9092

yml 파일은 여러 개로 작성해도 되고 하나로 작성해도 된다

ec2 서버에 올리는 게 생각보다 번거로워서 우선 하나로 관리했다

--- <- 이 기호를 기준으로 설정 파일을 분리해준다

 

3. deploy.sh

#!/bin/bash
echo "> 현재 구동중인 profile 확인"
CURRENT_PROFILE=$(curl -s {{https://도메인주소/api2/test/profile}})
echo "> $CURRENT_PROFILE"

if [ $CURRENT_PROFILE == prod1 ]
then
  IDLE_PROFILE=prod2
  IDLE_PORT=9092
elif [ $CURRENT_PROFILE == prod2 ]
then
  IDLE_PROFILE=prod1
  IDLE_PORT=9091
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> prod1을 할당합니다. IDLE_PROFILE: prod1"
  IDLE_PROFILE=prod1
  IDLE_PORT=9091
fi

IMAGE_NAME=app_server
TAG_ID=$(docker images | sort -r -k2 -h | grep "${IMAGE_NAME}" | awk 'BEGIN{tag = 1} NR==1{tag += $2} END{print tag}')

echo "> 현재 위치"
pwd

echo "> 도커 build 실행 : docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} ."
docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} .

echo "> $IDLE_PROFILE 배포"
echo "> 도커 run 실행 :  sudo docker run --name $IDLE_PROFILE -d --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}"
docker run --name $IDLE_PROFILE -d --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}

echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s {{https://도메인주소:$IDLE_PORT/actuator/health}} "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s {{https://도메인주소:$IDLE_PORT/actuator/health}})
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then
    echo "> Health check 성공"
    break
  else
    echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
    echo "> Health check: ${response}"
  fi

  if [ $retry_count -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> Nginx에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done

echo "> 스위칭을 시도합니다..."
sleep 5

echo "> 현재 위치..."
pwd

switch.sh

deploy.sh와 switch.sh는 같은 폴더에 위치해 있는 경우이다

(다른 폴더에 위치해도 상관 없는데 그럴 경우 마지막 줄에서 switch.sh 파일 경로를 반영해야 한다)

 

먼저 구동 중인 profile을 확인한다.

만약에 구동 중인 profile이 있으면 curl - s를 이용하여 api를 불렀을 때,

현재 실행되고 있는 profile의 이름이 뜰 것이고,

그렇지 않으면 에러가 날 것이다.

 

prod1이 실행 중이면 prod2를 실행할거고,

prod2가 실행 중이면 prod1을 실행할거다.

아무 것도 실행 중이지 않으면 prod1을 실행한다.

 

도커 이미지를 생성하기 위해 이미지 이름과 태그를 지정하고, 도커를 build하고 run으로 배포한다.

도커가 실행되는 데 약간의 시간이 걸릴 수 있으니 10초 뒤에 health check를 통해서 서버 상태를 확인한다.

health check가 성공하면 switch.sh를 실행하면서 기존 서버를 지우고 새로운 서버로 연결하는 작업을 한다.

health check에 실패하면 10번 정도 재시도한다.

 

 

4. switch.sh

#!/bin/bash
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s {{https://도메인주소/api2/test/profile}})

if [ $CURRENT_PROFILE == prod1 ]
then
  CURRENT_PORT=9091
  IDLE_PORT=9092
elif [ $CURRENT_PROFILE == prod2 ]
then
  CURRENT_PORT=9092
  IDLE_PORT=9091
else
  echo "> 일치하는 Profile이 없습니다. Profile:$CURRENT_PROFILE"
  echo "> 9091을 할당합니다."
  IDLE_PORT=9091
fi

echo "> 현재 구동중인 Port: $CURRENT_PORT"
echo "> 전환할 Port : $IDLE_PORT"
echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | tee /etc/nginx/conf.d/service-url.inc

echo "> ${CURRENT_PROFILE} 컨테이너 삭제"
docker stop $CURRENT_PROFILE
docker rm $CURRENT_PROFILE

echo "> Nginx Reload"

service nginx reload

현재 실행 중인 profile에 따라서 포트 번호를 정의한다.

아까 작성했던 /etc/nginx/conf.d/service-url.inc에 바꾼 주소를 입력한다.

기존의 docker를 멈추고 삭제한 뒤에 nginx를 reload 한다.

이 과정에서 약간의 502 error가 발생하는데,

무중단 배포를 수행하기 전과 후를 비교했을 때 사용자가 서비스를 사용할 수 없는 다운타임이 최소화 된 것을 알 수 있다.

(아예 없어진 게 아니라 최소화라고 하는 이유는 nginx가 reload 되는 약간의 seconds가 있다)

 

5. 최초 실행

docker build --build-arg IDLE_PROFILE=prod1 -t app_server:1 .
sudo docker run --name prod1 -d -p 9091:9091 app_server:1

파일 작성을 모두 완료했다면 다음과 같이 도커를 실행시키고,

Jenkins 빌드 시 deploy.sh를 실행하여 기존 서버는 내리고 새로운 서버를 올리도록 만든다.

 

docker ps -a로 실행된 도커 목록을 봤을 때

prod1에서 prod2f로 바뀐 것을 볼 수 있다

신나서 계속 Jenkins 실행하면서 테스트 해보았는데 굉장히 잘 되었따!

하면서 진짜 오류가 많이 났는데,

포트 오류로 502도 겪어보고,

내가 만든 보안에 내가 걸려서 401도 오랫 동안 헤맸었고,

Jenkins 자체도 설치하는 데 오류 나서 미러 서버 바꿔가면서 하고,

Jenkins를 도커로 빌드하는 바람에 도커 안에 도커 깔기 위해서 volume 설정도 다시 하고,

미러 서버 바꿔서 설치하는 와중에 Jenkins 버전 낮아서 버전 업데이트도 하고,

pipeline이 왜 실행 안 되고 멈추나 했는데 버전 문제였고,

webhook 걸었는데 푸시 테스트 하니까 502 에러나서 주소 확인 다시 하고,

Jenkins 내부에서 nginx 파일 접근 못해서 ssh publisher도 찾아보고,

서버는 1개인데 운영 서버 배포 / blue-green 서비스 테스트 동시에 할 수 있는 잔머리도 굴려봤다.

 

결국 최대한 로컬에서 많이 테스트 해보고,

nginx도 api2로 새롭게 빌드해서 테스트 해보고,

새벽에 서버 점검한다고 잠깐 기존 서버 내리고 새로운 서버 적용시켰다..

이후에는 에러 안 났음!!

 

무중단 배포 해야지 -> 어떻게 하지 -> 이것저것 해보기 -> 왜 안되는거지 -> 왜 되는거지 -> 된건가?

이 과정을 거쳐서 했는데 가장 어려웠던 건 Jenkins를 docker로 빌드하는 바람에 nginx에 접근 못 하는 문제...

prod1->prod2 도커 빌드까지 다 되는데 nginx 때문에 포기할 뻔 했다..ㅠㅠ

진짜진짜 오류가 너무 많이 나서 일주일 동안 힘들었는데 그래도 해내서 뿌듯했다.

728x90
Comments