본문 바로가기

Spring

Nginx 무중단 배포

728x90

항해 75일차

Travis CI와 CodeDeploy로 CI/CD 구성을 하였다.
배포 자동화까지 되었으니 무중단 배포까지 진행했다.

긴 시간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기 때문에 서비스가 종료된다.
서비스를 정지하지 않고 배포하는 것을 무중단 배포라고 한다.

Nginx가 가지고 있는 여러 기능 중 리버스 프록시가 있다.
리버스 프록시란 Nginx가 외부의 요청을 받아 백엔드 서버로 요청을 전달하는 행위를 이야기 한다.
리버스 프록시 서버(엔진엑스)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션들이 처리한다.

우리는 이 리버스 프록시를 통해 무중단 배포 환경을 구축해 볼 예정이며
Nginx를 이용한 무중단 배포를 하는 이유는 가장 저렴하고 쉽다.

구조

하나의 EC2 혹은 리눅스 서버에 엔진엑스 1대와 스프링 부트 Jar를 2대를 사용한다.

Nginx는 80(http), 443(https) 포트를 할당합니다.
스프링 부트1은 8081 포트로 실행
스프링 부트2는 8082 포트로 실행

운영 과정

사용자는 서비스 주소로 접속(80 혹은 443 포트).
Nginx는 사용자의 요청을 받아 현재 연결된 스프링 부트로 요청을 전달
스프링 부트1 즉, 8081 포트로 요청을 전달한다고 할때
스프링 부트2는 Nginx와 연결된 상태가 아니니 요청받지 못한다.
최신 버전으로 신규 배포가 필요하면, Nginx와 연결되지 않은 스프링 부트2(8082 포트)로 배포한다.

Nginx와 스프링 부트 연동

  • 먼저 EC2에 Nginx를 설치 및 실행 한다.

    sudo apt-get update
    sudo apt-get install nginx
    sudo service nginx start

  • Nginx가 현재 실행 중인 스프링 부트 프로젝트를 바라볼 수 있도록 프록시 설정을 먼저 해준다.

    sudo vi /etc/nginx/sites-enabled/default

  • 조금 내려가서 아래 이미지와 동일하게 설정해준다.

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

    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;

Nginx 설정 수정

  • 배포 때마다 엔진엑스의 프록시 설정(스프링 부트로 요청을 흘려보내는)이 순식간에 교체된다.
    여기서 프록시 설정이 교체될 수 있도록 설정을 추가
    Nginx 설정이 모여있는 /etc/nginx/conf.d/ 에 servie-url-inc라는 파일을 하나 생성

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

  • 생성 후 아래 내용을 추가해준다.

    set $service_url http://127.0.0.1:8081;
    저장하고 종료한(:wq) 뒤 Nginx 재시작
    sudo service nginx restart

profile API 추가 및 배포 스크립트 작성

  • ProfileController를 만들어 다음과 같이 간단한 API 코드를 추가합니다.
    해당 API는 이후 배포 시에 8081을 쓸지, 8082를 쓸지 판단하는 기준이 된다.
@RequiredArgsConstructor
@RestController
public class ProfileController {

    private final Environment env;

    /* NginX가 어느 포트의 서버를 바라보고있는지 확인하기 위한 컨트롤러 */
    @GetMapping("/profile")
    public String profile() {
        List<String> profiles = Arrays.asList(env.getActiveProfiles());
        List<String> realProfiles = Arrays.asList("real1", "real2");
        String defaultProfile = profiles.isEmpty() ? "default" : profiles.get(0);

        return profiles.stream()
                .filter(realProfiles::contains)
                .findAny()
                .orElse(defaultProfile);
    }
}
  • real1, real2, profile 생성

  • application-real1.properties

    server.port=8081
  • application-real2.properties

    server.port=8082
  • profile API 추가를 완료했다면 배포 스크립트를 작성해보자.

  • 먼저 기존 배포하였던 test와 중복되지 않기 위해 EC2에 test2 디렉토리를 생성한다.

    mkdir ~/app/test2 && mkdir ~/app/test2/zip

  • 무중단 배포는 앞으로 test2를 사용하고 appspec.yml 역시 test2로 배포되도록 수정한다.

    version: 0.0
    os : linux
    files :
    - source : /
      destination: /home/ubuntu/app/test2/zip
      overwrite : yes

무중단 배포를 진행할 스크립트들은 총 5개다.

  • stop.sh
    • 기존 Nginx에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료
  • start.sh
    • 배포할 신규 버전 스프링 부트 프로젝트를 stop.sh로 종료한 'profile'로 실행
  • health.sh
    • 'start.sh'로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크
  • switch.sh
    • 엔진엑스가 바라보는 스프링 부트를 최신 버전으로 변경
  • profile.sh
    • 앞선 4개 스크립트 파일에서 공용으로 사용할 'profile'과 포트 체크 로직
  • ppspec.yml 추가 수정

    hooks:
    AfterInstall:
      - location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료
        timeout: 60
        runas: ubuntu
    ApplicationStart:
      - location: start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작
        timeout: 60
        runas: ubuntu
    ValidateService:
      - location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인
        timeout: 60
        runas: ubuntu
  • profile.sh

    #!/usr/bin/env bash
    

bash는 return value가 안되니 제일 마지막줄에 echo로 해서 결과 출력후, 클라이언트에서 값을 사용한다

쉬고 있는 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
```java 
#!/usr/bin/env bash

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

IDLE_PORT=$(find_idle_port)

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

if [ -z ${IDLE_PID} ]
then
  echo "> 구동중인 PID : ${IDLE_PID}"
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  kill -15 ${IDLE_PID}
  sleep 5
fi
  • start.sh
    #!/usr/bin/env bash
    

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

REPOSITORY=/home/ubuntu/app/step3
PROJECT_NAME=hanghae8-admin

echo "> Build 파일 복사"
echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/"

cp $REPOSITORY/zip/*.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
-Dspring.profiles.active=$IDLE_PROFILE
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &


- health.sh
``` java
#!/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
    #!/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

 포트 전환하면서, 바꾼거말고 다른거 kill
if [ ${IDLE_PORT} == 8081 ]
then
  KILL_PORT=8082
  IDLE_PID=$(lsof -ti tcp:${KILL_PORT})
  echo "> ${KILL_PORT} 포트를 종료합니다."
  kill -15 ${IDLE_PID}
else
  KILL_PORT=8081
  IDLE_PID=$(lsof -ti tcp:${KILL_PORT})
  echo "> ${KILL_PORT} 포트를 종료합니다."
  kill -15 ${IDLE_PID}
fi

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

}

```

  • 깃허브 커밋 / 푸시로 확인해본다.

'Spring' 카테고리의 다른 글

예외 처리  (0) 2024.04.16
Travis CI로 application.properties 암호화  (0) 2024.04.16
Travis CI와 AWS S3, CodeDeploy 연동 (3)  (0) 2024.04.16
Travis CI와 AWS S3 연동 (2)  (0) 2024.04.16
Travis CI 연동 (1)  (0) 2024.04.16