[무중단 배포] Nginx + CodeDeploy + Github Actions

Intro

지금까지 CICD를 활용해 자동화된 통합과 배포에 대해 공부했다.

자동화에는 성공했지만 여전히 배포 과정에서 서버가 다운된다는 문제가 남아있기 때문에 서버 중단없이 배포하는 방법이 필요하다.

무중단 배포 방식에는 AWS Blue-Green, 도커를 이용한 무중단 배포 등 여러 가지가 있다.

그 중에서 NginX를 활용한 방법이 가장 비용이 적고 쉽기 때문에 NginX를 사용할 것이다. 

 

Nginx 무중단 배포 과정

Nginx는 프록시 서버이기 때문에 여러 개의 서버를 실행하고 Nginx가 서버들로 요청을 전달한다.

이때 서버가 업데이트 된다면 먼저 하나의 서버를 먼저 업데이트한다. 이때 다른 서버가 실행 중이므로 클라이언트들을 여전히 구버전의 서비스를 이용할 수 있다.

서버 업데이트가 끝나면 업데이트된 서버를 다시 Nginx에 reload하고, 나머지 서버에서도 같은 과정을 반복한다.

이렇게 하면 클라이언트가 서비스를 계속해서 사용하면서 서비스를 업데이트할 수 있게된다.

Nginx 설치 및 실행

 

EC2에 Nginx 설치

sudo yum install nginx

Nginx 버전 확인

sudo nginx -v

Nginx 실행

sudo service nginx start

상태 확인

sudo service nginx status

 

Nginx의 기본 포트번호는 80번이다. EC2에서 인바운드 규칙에 80번을 허용해놓고 접속해보면 아래와 같은 화면을 볼 수 있다.

이제 이 80번 포트를 통해서 요청을 주고받을 것이기 때문에 기존에 API 리다이렉션 주소가 있다면, 전부 80번으로 바꿔주어야한다.

 

Nginx 설정

Nginx 설치가 끝났으니, Nginx가 실행 중인 서비스를 바라볼 수 있게 설정해주어야한다.

 

Nginx  설치 후 /etc/nginx/ 에 Nginx 설치 파일들이 존재하며, nginx.conf 파일을 통해 설정을 할 수 있다.

 

 

service-url.inc 파일을 생성해서 아래 내용 작성

sudo vim /etc/nginx/conf.d/service-url.inc
set $service_url http:127.0.0.1:8080;

 

이후 파일 수정을 위해 sudo 권한으로 nginx.conf 파일을 열어준 다음 내용 작성

sudo vim /etc/nginx/nginx.conf
include /etc/ngnix/conf.d/service-url.inc;

location / {
        proxy_pass          $service_url;
        proxy_set_header    X-Real_IP $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header    Host $http_Host;
}

proxy_pass : 요청이 들어왔을 때 전달할 URL

proxy_set_header XXX: header의 각 항목을 할당.

 

수정이 완료되면 restart 해준다.

sudo service nginx restart

 

서비스를 실행하고 80포트로 접속해보면 해당 서비스가 켜지는 것을 확인할 수 있다.

 

 


배포 스크립트 작성

appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step3/freelec-springboot2-webservice/ # 프로젝트 이름
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  AfterInstall:
    - location: /scripts/stop.sh
      timout: 60
      runas: ec2-user
  ApplicationStart:
    - location: /scripts/start.sh
      timeout: 60
      runas: ec2-user
  ValidateService:
    - location: /scripts/health.sh
      timeout: 60
      runas: ec2-user

 

쉘 스크립트 작성

profile.sh

#!/usr/bin/env bash
#(주의) 1번 라인 코드는 어떤 인터프리터로 해석해야하는지 명시 해놓은 'shebang'라인으로 나는 이 위에
#주석으로 설명을 달았다가 profile.sh 파일을 다른 sh 파일에서 인식을 하지 못하는 문제가 생겼다. 

# 쉬고있는 profile 찾기: real1이 사용 중이면 real2가 쉬고있고, 반대면 real1이 쉬고있음

function find_idle_profile() {
    RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)

    if [ ${RESPONSE_CODE} -ge 400 ] # 400보다 크면 (즉, 40x/ 50x 에러 모두 포함)
    then
        CURRENT_PROFILE=real2
    else
        CURRENT_PROFILE=$(curl -s http://localhost/profile)
    fi

    if [ "${CURRENT_PROFILE}" == "real1" ]
    then
        IDLE_PROFILE=real2
    else
        IDLE_PROFILE=real1
    fi

    echo "${IDLE_PROFILE}"
}

# 쉬고있는 profile의 port 찾기
function find_idle_port() {
    IDLE_PROFILE=$(find_idle_profile)

    if [ "${IDLE_PROFILE}" == "real1" ]
    then
        echo "8081"
    else
        echo "8082"
    fi
}

stop.sh

#!/usr/bin/env bash

#기존 엔진엑스에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)  # stop.sh가 속해있는 경로를 찾는다.
source ${ABSDIR}/profile.sh

IDLE_PORT=$(find_idle_port)

echo "> $IDLE_PORT 에서 구동 중인 애플리케이션 pid 확인"
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})

if [ -z ${IDLE_PID} ]
then
  echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  kill -15 ${IDLE_PID}
  sleep 10 # 종료 후 10초 대기 (종료 시간은 애플리케이션에 따라 조절 가능)
fi

start.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=freelec-springboot2-webservice

echo "> Build 파일 복사"
echo "> cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/"
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/

echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR_NAME: $JAR_NAME"

echo "> $JAR_NAME 에 실행권한 추가"
chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"
IDLE_PROFILE=$(find_idle_profile)

echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다"
nohup java -jar \
  -Dspring.config.location=classpath:/application.properties,classpath:/application-$IDLE_PROFILE.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
  -Dspring.profiles.active=$IDLE_PROFILE \
  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

health.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh


IDLE_PORT=$(find_idle_port)

echo "> Health Check Start!"
echo "> IDLE_PORT: $IDLE_PORT"
echo "> curl -s http://localhost:$IDLE_PORT/profile"
sleep 10

for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo "${RESPONSE}" | grep 'real' | wc -l)

  if [ ${UP_COUNT} -ge 1 ]
  then # $up_count >=1 ("real" 문자열이 있는지 검증)
    echo "> Health Check 성공"
    switch_proxy
    break
  else
    echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
    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

switch.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

function switch_proxy() {
    IDLE_PORT=$(find_idle_port)

    echo "> 전환할 Port: $IDLE_PORT"
    echo "> Port 전환"
    echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

    echo "> 엔진엑스 Reload"
    sudo service nginx reload
}

 

실행

git push 후 ec2에서 아래 명령을 입력해서 codeDeploy 로그를 볼 수 있다.

로그를 볼 때 잘려서 나온다면 -n 옵션으로 원하는 라인 수를 입력하면된다. (디폴트는 10이다)

tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log

 

Reference

스프링부트와 AWS로 혼자 구현하는 웹 서비스

https://wbluke.tistory.com/41