Web/Ops

무중단 배포 - 롤링 배포 (2/4)

jodong2 2023. 3. 5. 16:24

발그림ㅎㅎ;

롤링 배포 - 개념

 롤링 배포는 현재 사용 중인 인스턴스 내에서 새 버전으로 하나씩 교체해나가는 것이다.

 롤링 배포를 이용하게 된다면 기존에 유지하고 있는 인스턴스로만 진행하기 때문에 추가적으로 비용이 발생하지않는다. 또한 점진적으로 업데이트하기때문에 롤백이 비교적 쉽다. 하지만 새로운 버전으로 교체하는 과정에서 일부 인스턴스가 연결되어 있지 않기 때문에 트래픽을 감당할 수 있는 선에서 진행해야하며 이전 버전과 새로운 버전의 호환성 문제도 생각해야 한다. 

 

롤링 배포 - 실습

 

1. spirng actuator 설정

build.gradle

다음과 같이 build.gradle에 spring actuator 의존성을 추가한다. 

spring actuator는 서버의 여러가지 상태를 확인할 수 있는 기능이다. 하지만 민감함 정보도 포함될 수 있으므로 우리가 필요한 health만 사용하도록 yml(properties)파일을 수정해주자. 

application.properties

설정 후 /actuator/health 를 확인하면 {"start":"UP"} 을 확인할 수 있다.

 

2. nginx 설정

nginx pro버전에서는 health-check 기능을 지원하지만, 내 지갑은 휑하므로 무료 health-check 모듈이 포함된 nginx를 사용할 것이다. 

먼저 nginx 설정 파일을 만들어보자 

vi nginx.conf

events {}

http {
  upstream market {
	# 배포 중인 서버들
	server ip:port;
	server ip:port;

	#health-check
	# interval - 3초씩에 한 번씩 확인
	# rise - 2번 이상 응답에 성공하면 서버가 살은 것으로 판단
	# fall - 5번 이상 응답에 실패할 경우 서버가 죽은 것으로 판단
	# timeout - 응답 시간 초과 1초
	check interval =3000 rise=2 fall=5 timeout=1000 type=http;
	# GET /actuator/health으로 healthcheck 요청
	check_http_send "GET /actuator/health HTTP/1.0\r\n\r\n";
	check_http_expect_alive http_2xx http_3xx;
  }

  server {
    listen 80;
    location / {
       proxy_pass http://market;
    }
    # 서버 상태 확인 url
     location /status {
     check_status;
   }
  }
}

다음 nginx image를 빌드하기 위해 Dockerfile을 작성한다. 

vi Dockerfile

# mrlioncub/nginx_upstream_check_module - health-check 모듈이 포함된 nginx image
FROM mrlioncub/nginx_upstream_check_module
COPY nginx.conf /etc/nginx/nginx.conf

Docker file을 작성했으니 이미지를 빌드하고 실행하자 

docker build --tag nginx:test-nginx .
docker run -d --name webserver -p 80:80 [tag | image id]

 

3. Shell script 작성

  • jenkins에서 배포 후에 실행할 shell script를 서버에 작성해두자
  • rollingdeploy.sh의 전체적인 흐름은 다음과 같다.
    • 현재 배포하려는 서버 외에 트래픽을 받을 수 있는 서버, health-check 없다면 배포하지 않음
    • 현재 tomcat server 종료 gracefully shut down -> force shut down
    • 배포 시작
    • 배포 후 자가 health-check

 

더보기
BASE_PATH=/home/app/
JAR_NAME=xxx.jar
echo "> build 파일명: $JAR_NAME"

#배포중인 server ip
IP1=# 
IP2=# 
#배포 port
DEPLOYED_PORT=8080


#private ip
MY_IP=$(hostname -i)

loop=1
limitLoop=30
flag='false'


if [ $MY_IP == $IP1 ]; then
  OTHER_IP=$IP2
elif [ $MY_IP == $IP2 ]; then
  OTHER_IP=$IP1
else
  echo "> 일치하는 IP가 없습니다. "
fi

#==========================살아있는 서버가 존재하는지 확인==============================
echo "> 서버 체크 시작"
for retry_count in {1..10};
do
  response=$(sudo curl -s http://$OTHER_IP:$DEPLOYED_PORT/actuator/health)
  up_count=$(echo $response | grep 'UP' | wc -l)
  echo "> $retry_count : $response  : $up_count"
  if [ $up_count -ge 1 ]; then
    echo "> 서버 health 체크 성공"
    break
  fi
  if [ $retry_count -eq 10 ]; then
    echo "> 서버 health 체크 실패"
    exit 1
  fi
  echo "> 실패 10초후 재시도"
  sleep 10
done

#===================================프로세스 종료======================================
# tomcat gracefully shutdown
echo "> 구동중인 애플리케이션 pid 확인"
IDLE_PID=(`ps -ef | grep  $JAR_NAME | grep -v 'grep' | awk '{ print $2 }'`)
 if [ ${#IDLE_PID[@]} = 0 ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
  flag='true'
else
  for pid in "${IDLE_PID[@]}"
  do
      echo "> [$pid] gracefully shutdown"
      kill -15 $pid
  done
  while [ $loop -le $limitLoop ]
  do
      PID_LIST=(`ps -ef | grep  $JAR_NAME | grep -v 'grep' | awk '{ print $2 }'`)
      if [ ${#PID_LIST[@]} = 0 ]
      then
          echo "> gracefully shutdown success "
          flag='true'
          break
      else
          for pid in "${PID_LIST[@]}"
          do
              echo "> [$loop/$limitLoop] $pid 프로세스 종료를 기다리는중입니다."
          done
          loop=$(( $loop + 1 ))
          sleep 1
          continue
      fi
  done
fi
if [ $flag == 'false' ];
then
    echo "> 프로세스 강제종료 시도"
    sudo ps -ef | grep $JAR_NAME | grep -v 'grep' |  awk '{ print $2 }' | \
    while read PID
    do
        echo "> [$PID] forced shutdown"
        kill -9 $PID
    done
fi

#===================================배포======================================

echo "> 배포"
echo "> 파일명" $BASE_PATH$JAR_NAME
sudo nohup java -jar -Dspring.profiles.active=prod $BASE_PATH$JAR_NAME & 
sudo sleep 10

echo "> 10초 후 Health check 시작"
echo "> curl -s http://$MY_IP:$DEPLOYED_PORT/actuator/health"

#==========================현재 서버 Health check============================
for retry_count in {1..10}; do
  response=$(sudo curl -s http://$MY_IP:$DEPLOYED_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 연결 실패. 재시도..."
  sudo sleep 10
done

sleep 60 # 다음 배포 서버를 위한 지연

 

4. Jenkins에서 배포 시 deploy.sh 실행.