nginX 무중단 배포 실습

2022. 3. 9. 09:11개발/실습

0. 구성


2개의 애플리케이션을 사용한다. 사용자는 포트 80번을 사용하는 nginx로 요청을 보내고 nginx는 현재 연결된 애플리케이션을 사용자에게 제공하도록 한다. 

포트 8081을 사용하는 애플리케이션으로 포워딩 하는 nginx

만약 포트 8082를 사용하는 애플리케이션이 신규로 배포되었다면 nginx는 포트 8082의 애플리케이션을 사용자에게 제공할것이다.

포트 8082를 사용하는 애플리케이션이 신규 배포 된 경우


 

1. nginX 설치 및 수정


sudo amazon-linux-extras install nginx1 명령어를 통해 nginx를 설치한다.

설치 시작

sudo systemctl nginx start로 nginx를 시작하며 sudo systemctl status nginx로 현재 nginx의 상태를 확인할 수 있다.

상태확인

nginx의 기본 포트는 80이기 때문에 EC2의 보안그룹의 인바운드를 편집한다.

모든 IP의 TCP 80번 포트를 열어준다.

nginx가 정상적으로 동작하는지 확인한다. 주소에 포트번호를 생략하면 80번 포트로 접속하게 된다.

nginx 동작 확인

/etc/nginx 경로에 있는 nginx.conf 파일을 연다. 

nginx.conf는 nginx에 대한 기본적인 설정을 할 수 있는 파일이다.

 

아래의 사진을 보면서 nginx에서 사용하는 context에 대해서 간단히 알아본다.

events context는 전역(Global) 옵션을 설정하는 Context다. NginX Configuration을 작성할때 하나만 존재하도록 해야한다.

http context는 events contex와 형제 context이며 main context를 부모로 가진다 따라서 http와 event context는 아래 처럼 side-by-side하게 작성하는것이 옳다.

server context는 http context내에 정의된다. server context는 multiple한 정의가 가능하다. 선언한 server context의 개수에 따라 NginX는 가상서버를 만들어 준다.

server context에 있는 listen은 server에 대한 접근 조건을 관리한다. IP와 Port로 이뤄져있다.

server context에 있는 server_name은 서로 다른 server에 똑같은 listen이 작성되어 있을때 사용된다. 만약 서로 다른 server에 같은 listen이 작성되어 있다면 NginX는 server_name과 request의 헤더에 있는 Host 필드를 확인한다. 만약 server_name과 Host 필드가 같다면 해당 요청을 해당 server가 처리할 수 있도록 한다.

location context는 server context 하위에 multiple한 정의가 가능하다. 사용자의 요청과 매칭이 되는 location이 선택되면 하위에 작성된 경로로 forwarding 해준다.

 

location context를 보면 /로 들어오는 모든 요청은 localhost의 8080포트로 포워딩됨을 알 수 있다.

하지만 위처럼 작성하면 새로운 proxy_pass를 동적으로 제어하지 못한다. 따라서 아래와 같이 service-url.inc 파일을 import해서 proxy_pass에서 사용할 url을 불러오는 방식으로 코드를 변경한다.

 

/etc/nginx/conf.d에 있는 service-url.inc의 내용을 작성한다. nginx.conf파일에서 사용할 service_url 변수 값을 여기서 설정한다. 이런식으로 코드를 구성하면 nginx.conf의 proxy_pass를 runtime시에 변경이 가능하다.

service-url.inc에 작성한 내용 확인


2. profile 생성


application-real1.yml과 application-real2.yml을 생성한다. application-{profile명}.yml 규칙에 따라 profile 명은 real1과 real2가 된다

profile 목록

두 profile의 차이점은 server의 포트뿐이다. real1은 8081, real2는 8082를 사용하도록 했다.

real1
real2

 


3. 셸 스크립트 작성


codedeploy를 통해 배포를 수행할 예정이므로 appspec.yml을 다음과 같이 작성한다. 

version: 0.0
os: linux
files:
  - source: / # 어떤 파일들을 보낼것인가. 루트 하위에 있는 모든 파일을 보내겠다
    destination: /home/ec2-user/toy-project-board/deploy/project # 타겟 EC2의 어떤 경로에 저장할지
    overwrite: yes # 기존에 파일이 있다면 덮어쓴다.
hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: root
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 300
      runas: root
  ValidateService:
    - location: scripts/health.sh
      timeout: 300
      runas: root

 

appspec.yml에 있는 스크립트뿐만아니라 쉬고 있는 profile과 port를 찾기위한 profile.sh, nginx의 포워딩 주소 변경을 위한 switch.sh을 추가로 작성한다.

 

작성한 스크립트

 

먼저 profile.sh를 살펴보면, find_idle_profile 함수에선 현재 사용중이지 않은 profile을 찾고, find_idle_port 함수에서는 현재 사용중이지 않은 port를 찾는다.

#!/usr/bin/env bash

function find_idle_profile()
{
    # 현재 활성화된 profile 찾기
    RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
  
    # 400 보다 크면(에러)다면
    if [ ${RESPONSE_CODE} -ge 400 ] 
    then
        CURRENT_PROFILE=real2 # 현재 활성화된 profile이 없다면 real2로 매핑한다
    else
        CURRENT_PROFILE=$(curl -s http://localhost/profile) # 현재 활성화된 profile
    fi


    if [ ${CURRENT_PROFILE} == real1 ] # 현재 활성화된 profile이 real1이면
    then
      IDLE_PROFILE=real2 # real2가 쉬는중
    else
      IDLE_PROFILE=real1
    fi

    echo "${IDLE_PROFILE}"
}

# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
    IDLE_PROFILE=$(find_idle_profile) # 현재 쉬고 있는 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) # ABSPATH가 있는 디렉토리 (ABSPATH는 파일명이 포함된 경로이기때문)
source ${ABSDIR}/profile.sh # 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 5
fi

 

stop.sh를 통해서 현재 쉬고 있는 애플리케이션을 종료 했다면 start.sh를 통해서 현재 쉬고 있는 애플리케이션을 다시 실행시킨다.

Dspring.profiles.active를 통해 애플리케이션을 실행할때 활성화할 profile을 선택한다.

Dspring.config.location을 통해 사용할 profile들을 불러온다. 별다른 설정을 하지 않았다면 classpath: 는 src/main/resources/ 와 src/main/java/ 가 된다.

#!/usr/bin/env bash

JAR_PATH=/home/ec2-user/toy-project-board/deploy
PROJECT_PATH=/home/ec2-user/toy-project-board/deploy/project
ABSPATH=$(readlink -f $0) # 현재파일 절대경로 (링크가 있다면 실제 경로를 찾도록한다)
ABSDIR=$(dirname $ABSPATH) # ABSPATH가 있는 디렉토리 (ABSPATH는 파일명이 포함된 경로이기때문)
source ${ABSDIR}/profile.sh # profile.sh를 실행한다.


echo "> Start Build"

cd $PROJECT_PATH # 프로젝트가 저장된 경로로 이동
sudo chmod ugo+rwx gradlew # 권한부여
sudo ./gradlew build # 빌드시작

echo "> copy Jar"
cp $PROJECT_PATH/build/libs/*jar $JAR_PATH/ # jar 파일을 복사

JAR_NAME=$(ls -tr $JAR_PATH/*.jar | tail -n 1) # 실행할 Jar명 가져오기

echo "> Run $JAR_NAME"

IDLE_PROFILE=$(find_idle_profile)

echo ">Run $JAR_NAME with IDLE_PROFILE"
echo ">IDLE_PROFILE $IDLE_PROFILE"

# application-db.yml 로딩 및 $IDLE_PROFILE profile 선택
nohup java -jar -Dspring.config.location=/home/ec2-user/toy-project-board/application-db.yml, \
      classpath:/application-$IDLE_PROFILE.yml \
      -Dspring.profiles.active=$IDLE_PROFILE $JAR_NAME > $JAR_PATH/nohup.out 2>&1 &

 

health.sh에선 start.sh에서 실행한 애플리케이션이 정상적으로 동작하는지 확인한 후 nginx에서 start.sh에서 실행한 애플리케이션을 바라 볼수 있도록 switch.sh에 있는 switch_proxy 함수를 실행시킨다.

 

#!/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 "> 엔진엑스에 연결하지 않고 배포를 종료합니다"
    exit 1
  fi

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

 

switch.sh는 tee 명령어를 사용해 echo에 입력된 문자열을 service-url.inc에 그대로 덮어쓰기 한다.

#!/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 "> 엔진엑스 재시작"
  sudo systemctl nginx reload

}

동작확인


 Jenkins를 통해 2회 이상 코드를 배포하면 아래 사진처럼 2개의 애플리케이션이 모두 실행중인 것을 확인할 수 있다.

두개의 Application이 실행되고 있는 모습

다만 service-url.inc를 확인해보면 nginx는 8081번 포트와 연결되어 있음을 확인할 수 있다. 즉, 8082번 포트는 현재 사용자에게 서비스가 되고 있지 않는 상태이다.

 


발생한 문제


jenkins를 통해 새로운 코드를 배포를 해도 codedeploy가 계속 과거에 있던 코드를 빌드하는 현상이 있었다.  그래서 /opt/codedeploy-agent/deployment-root/6bb1bb58-3259-442f-a653-53a0ca1a6d89 배포그룹 하위에 있는 기존 소스를 모두 지웠다. 

배포ID별 디렉토리

이후 codedeploy시 아래와 같은 에러가 발생했다. 이때 메시지에 있는 경로를 신규로 만들고 appspec.yml 파일을 함께 만들어줬더니 배포시 신규 코드로 잘 작동했다.


참고 서적 및 사이트


 

 

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

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

Nginx Configuration 파일 분석

 

Understanding the Nginx Configuration File Structure and Configuration Contexts | DigitalOcean

 

www.digitalocean.com

 

 

SC2086

 

GitHub - koalaman/shellcheck: ShellCheck, a static analysis tool for shell scripts

ShellCheck, a static analysis tool for shell scripts - GitHub - koalaman/shellcheck: ShellCheck, a static analysis tool for shell scripts

github.com

 

classpath에 대한 개념 정리 사이트

 

Classpath - Wikipedia

Classpath is a parameter in the Java Virtual Machine or the Java compiler that specifies the location of user-defined classes and packages. The parameter may be set either on the command-line, or through an environment variable. Overview and architecture[e

en.wikipedia.org

 

 

자바 클래스패스

이전부터 스프링으로 웹 서비스를 개발할 때 테스트 코드를 짜는 것에 대한 중요성은 일찍히 알고 있었다. 테스트 코드를 만들다 보면 항상 봉착하는 문제가 있었는데 내가 설정한 빈 파일 혹은

vvshinevv.tistory.com