본문 바로가기

인프라

Nginx를 이용한 로드 밸런싱, 무중단 배포까지의 고찰

로드 밸런싱 적용, 덤으로 무중단 배포까지

왜 로드밸런싱?

로드 밸런싱, 무중단 배포 설계에 대한 내용만을 중점적으로 보여주기 위해 
앞 단의 프론트 인프라 구조와 DB Replication 적용 후의 구조는 잠시 생략한다.

로드 밸런싱을 적용하기 전, 웹서버 한대는 WAS 하나를 가리키고 있었다. 
이 경우 수많은 요청이 오더라도, 우리는 서버 한대 분량의 부하만을 견딜 수 있었다. 

하지만, 놀토의 사용자가 늘고 또 이 사용자들이 동시적으로 혹은 처리 비용이 높은 트래픽이 많아질 경우를 고려할 필요가 있다. 
이 때 사용할 수 있는 기술이 로드 밸런싱인데 간단히 말하면 여러대의 서버에 (여러 고려사항을 적용해) 적절하게 요청을 분산하는 기술이다.

장점

  1. 한 대의 서버의 부하를 분산시킬 수 있다.
  2. 한 대의 서버가 멈추더라도 중단 없이 다른 서버가 서비스를 계속 유지할 수 있다.

우리는 기존에 웹서버로 사용하고 있던 Nginx에 간단한 설정만으로 로드밸런싱을 구현할 수 있다.

Nginx를 사용해 로드 밸런싱 적용하기

서버를 하나 더 증설시켜 서버 두 대로 놀토 서비스를 운영할 것이기 때문에 기존 WAS에 이어 새로운 WAS 서버를 만들어 준다. 

후에 nginx 설정 파일인 nginx.conf를 vim을 사용해 편집해준다. 
우리 같은 경우에는 도커를 사용해 nginx를 띄웠는데, 도커 밖에서 nginx.conf를 작성하고 COPY하는 방식이기 때문에,
ec2 /home/ubuntu에 해당 설정 파일이 위치해있다. (후에 Dockerfile 을 보면 이해가 될 것)
기본적으로는 /etc/nginx/nginx.conf에 위치해있다.

$ vim /etc/nginx/nginx.conf

nginx.conf 파일 http 블럭에 다음과 같은 설정을 추가해주면, 손쉽게 로드 밸런싱 설정을 할 수 있다.

upstream {Upstream name} { # 업스트림 서버를 정의하는 블럭
  {Load Balancing Method} # 로드 밸런싱 메서드 지정
  server {WAS 1 private IP}:{port} ; # 요청을 분산시킬 서버의 ip:port를 지정
  ...
  server {WAS 1 private IP}:{port} weight=5; # weight 값으로 서버의 가중치를 줄 수 있음 (기본값 1)
}

 

Nginx 로드 밸런싱 메서드

Round Robin (라운드 로빈)

  • 서버의 가중치를 고려해, 실제 서버들을 처음부터 차례로 선택해 가며 모든 서버로 균등하게 분산 됨 (default)
  • 장점 : 거의 균등하게 분산 가능
  • 단점 : 경로가 보장 되지 않음

Least-connected

  • 서버의 가중치를 고려해, 활성 연결 수가 가장 적은 서버로 요청을 전송 함
  • 장점 : 거의 균등하게 분산 가능
  • 단점 : 경로 보장 되지 않음

ip-hash

  • Hasing key 를 사용하여 IP 별로 Index 를 생성하여 경로를 지정
  • 요청이 전송되는 서버는 클라이언트 IP 주소에서 결정됨
  • IPv4 주소의 처음 세개의 octets 또는 전체 IPv6 주소가 해시 값을 계산하는 데 사용됨
  • 장점 : 경로 보장 가능
  • 단점 : 균등한 분산이 어려움

놀토에서는 클라이언트의 IP가 리프레시 토큰을 사용하는데 쓰이며, 이 값은 내장 Redis에 저장되고 있다.
때문에 한 클라이언트는 계속해서 같은 서버의 요청 처리를 받아야하기 때문에 ip-hash 방법을 사용하였다.

(2021.11.15) 레디스를 별도의 인스턴스로 분리함으로써 ip_hash 방식이 아닌 라운드로빈 방식을 가져가게 되었다.

Nginx 공식 문서- HTTP Load Balancing에서 더 다양한 로드 밸런싱 메서드들을 자세히 확인할 수 있다.

클라이언트 IP 정보를 유지하기 위한 설정

현재 Nginx 는 프록시 서버 역할을 하며 모든 클라이언트 요청이 이 프록시 서버를 거쳐온다. 
이 때 특별한 설정이 없으면 스프링 WAS에서 요청을 보낸 IP를 확인할 시 프록시인 현재 Nginx의 IP가 나오게 된다. 
우리는 클라이언트의 IP로 리프레시 토큰을 관리하게 되는데, 이 경우 모든 IP가 Nginx의 IP가 되기 때문에 IP로 관리할 수 없는 문제가 생긴다.
때문에 프록시 서버를 거쳐오더라도, 요청을 보낸 클라이언트의 IP가 유지되는 정보가 필요하다.

X-Forwarded-For

  • 프록시나 로드 밸런서를 통해 들어온 요청에서 클라이언트의 원 IP 주소를 확인하기 위해 사용하는 헤더값
  • X-Forwarded-For 헤더값을 설정하도록 하여 서버에서 클라이언트 IP를 확인할 수 있음
$proxy_add_x_forwarded_for?  
"X-Forwarded-For" 클라이언트 요청 헤더 필드에 $remote_addr가 추가되고 쉼표로 구분됩니다.
"X-Forwarded-For" 필드가 클라이언트 요청 헤더에 없으면
$proxy_add_x_forwarded_for 변수는 $remote_addr 변수와 같습니다 

nginx.conf 파일

events {}

http {
  upstream app {
    ip_hash;
    server {WAS 1 private IP}:{port};
    server {WAS 2 private IP}:{port};
  }

  #로그 설정
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  # Redirect all traffic to HTTPS
  server {
    listen 80;

    return 301 https://nolto.kro.kr;
  }

  server {
    listen 443 ssl;
    client_max_body_size 10M;
    ssl_certificate /etc/letsencrypt/live/nolto.kro.kr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nolto.kro.kr/privkey.pem;

    # Disable SSL
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # 통신과정에서 사용할 암호화 알고리즘
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    # Enable HSTS
    # client의 browser에게 http로 어떠한 것도 load 하지 말라고 규제합니다.
    # 이를 통해 http에서 https로 redirect 되는 request를 minimize 할 수 있습니다.
    add_header Strict-Transport-Security "max-age=31536000" always;

    # SSL sessions
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    location / {

      proxy_set_header X-Forwarded-For $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;

      # ... 생략

      return 403;
    }
  }
}

nginx Reload

변경된 설정을 반영하기 위해서는 nignx 서버를 재구동 시켜야한다. 
우리 같은 경우는 도커를 이용해 nginx를 구동시키고 있으니 다음과 같은 명령어를 모아놓은 sh을 실행해 nginx를 reload 한다. 

restart.sh

sudo docker stop proxy && sudo docker rm proxy # proxy는 nginx의 컨테이너 name
sudo docker build -t nolto/reverse-proxy:0.0.5 .
sudo docker run -d -p 80:80 -p 443:443 --name proxy nolto/reverse-proxy:0.0.5

여기까지, 로드 밸런싱과 관련된 설정을 마쳤다. 
한 서버가 잠시 중단 되었을 때 로드 밸런서가 알아서 살아있는 서버에만 요청을 전송하기 때문에
배포 시 두 서버에 차례대로 배포를 진행한다면, 한 서버가 배포 과정을 진행하는 동안 다른 서버를 유지하여
배포 도중에도 서비스를 계속 유지하는 무중단 배포도 어느정도 만족시킬 수 있다.

하지만 이 방식에는 몇가지 아쉬운 점이 존재한다. 


무중단 배포

로드밸런싱이 적용된 현재 구조에서의 무중단 배포 방식을 논하기 전, 무중단 배포의 개념과 다양한 아키텍쳐를 먼저 간단히 살펴본다.

왜 무중단 배포?

애플리케이션이 업데이트 되고, 이를 운영환경에 배포할 경우 배포하는 시간 동안 서비스는 중지될 수 밖에 없다. 
사용자가 여러 시간 대에 자주 접하는 서비스라면, 배포라는 이벤트는 사용자에게 잠깐의 서비스 중단이라는 불편을 불러올 수 있다. 
또한 만약 배포가 진행되다가 애플리케이션 빌드 자체가 실패해버리면, 이를 복구하는 방법 조차 어렵기 때문에 많은 문제가 있었다. 

이런 문제를 해결하기 위해 우리는 무중단 배포라는 것을 도입한다. 

무중단 배포 아키텍쳐

참고자료 - 무중단 배포 아키텍처(Zero Downtime Deployment) - 글로벌 서비스 운영의 필수 요소

롤링 배포(Rolling Deployment)

  • 무중단 배포의 가장 기본적인 방식
  • 사용 중인 인스턴스 내에서 새 버전을 점진적으로 교체하는 방식
  • 서비스 중인 인스턴스 하나를 로드밸런서에서 제거한 뒤, 새 버전을 배포 후 다시 라우팅하도록 하는 과정을 반복하여 모든 인스턴스에 새 버전의 애플리케이션을 배포
  • 인스턴스마다 차례로 배포를 진행하기 때문에 상황에 따라 손쉽게 롤백이 가능한 장점
  • 새 버전을 배포할 때 로드 밸런싱에 연결된 인스턴스 수가 감소하기 때문에 서비스 처리 용량을 고려해야 함
  • 또한 서버가 여러대일 경우 배포가 진행되는 동안 구버전과 신버전이 공존하기 때문에 호환성 문제가 발생할 수 있음

블루-그린 배포(Blue-Green Deployment)

  • 블루를 구버전, 그린을 신버전으로 지칭
  • 신규 서버가 배포 완료 상태가 되기 까지 기존 서버다 동작하다, 신규 서버가 준비되면 로드 밸런서의 방향을 변경
  • 배포 속도가 빠르며, 장애가 발생했을 때 로드 밸런서가 기존 서버를 가리키면 되기 때문에 롤백이 쉬움
  • 추가적인 서버로 인한 비용이 단점

 

현재 로드 밸런싱을 적용하므로써 자연스럽게 Rolling Deployment 구조를 가져가게 되었는데, 
앞서 말한 단점들을 문제 삼아 블루-그린 방식을 조금 차용해 적용할 수 있다. 

(배포 시에도 로드밸런싱은 유지되도록 (서비스 가능한 WAS가 2개가 되도록))

사실 무중단 배포를 적용하는 방법은 다양하지만,
우리는 가장 저렴하게 사용할 수 있으며 기존에 사용하고 있던 Nginx를 이용해 구현할 것이다.

서버 인스턴스는 두개만 사용하며, 각각의 인스턴스에서 포트를 다르게 띄운 WAS 두개를 가지고 있도록 했다. 

Nginx를 사용해 무중단 배포 적용하기

맨 처음에 구상한 흐름은 다음과 같다. 

하지만 이 방법은 기각이다. 
시행착오의 과정이기 때문에 접어두기 식의 글로만 남기려 한다 ㅎ
궁금하면 펼쳐보시길...

더보기

젠킨스에서 각각의 인스턴스에서 다음과 같은 작업을 진행한다. 

  1. 빌드한 jar 파일 전송
  2. 사용하고 있는 기존 port를 "/infra/port"를 통해 확인 
  3. 사용하지 않는 신규 port로 애플리케이션 배포 
  4. 신규 port로 "/infra/health"를 통해 헬스 체크
  5. 기존 port를 kill

시행착오의 WAS 스크립트

REPOSITORY=/home/ubuntu/nolto2021
PROJECT=nolto

cd $REPOSITORY/

echo "> application 디렉터리로 이동"

echo "> 현재 구동중인 PORT 상태 확인"
WAS_ONE_HEALTH=$(curl -s http://localhost:8080/infra/health)
WAS_TWO_HEALTH=$(curl -s http://localhost:8000/infra/health)
echo "> 8080: $WAS_ONE_HEALTH , 8000: $WAS_TWO_HEALTH"

if [ "$WAS_ONE_HEALTH" == "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8080입니다."
        CURRENT_PORT=8080
        TARGET_PORT=8000
elif [ "$WAS_TWO_HEALTH" == "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8000입니다."
        CURRENT_PORT=8000
        TARGET_PORT=8080
else
        echo "> 현재 구동 중인 애플리케이션의 포트를 찾는데 실패하였습니다."
        echo "> 8080 포트를 할당합니다."
        CURRENT_PORT=8000
        TARGET_PORT=8080
fi


CURRENT_PID=$(ps -ef | grep java | awk '{print $2}')
echo "> 현재 구동중인 애플리케이션 pid 확인 port: $CURRENT_PORT pid:  $CURRENT_PID"

echo "> 새 애플리케이션 배포 - 포트번호 : $TARGET_PORT"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=local -Dserver.port=$TARGET_PORT $REPOSITORY/$JAR_NAME 1> log.txt 2>&1 &

echo "> $TARGET_PORT 10초 후 Health check 시작"
echo "> curl -s http://localhost:$TARGET_PORT/infra/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$TARGET_PORT/infra/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      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 "기존 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID

        while kill -0 $CURRENT_PID; do
                sleep 5
                echo "> 프로세스가 아직 종료되지 않았습니다."
        done
        echo "> $CURRENT_PID 종료 완료"
fi

nginx.conf 파일

echo "> 현재 구동중인 PORT 상태 확인"
WAS_ONE_8080_HEALTH=$(curl -s http://52.78.132.99:8080/infra/health)
WAS_ONE_8000_HEALTH=$(curl -s http://52.78.132.99:8000/infra/health)
echo "> 8080: $WAS_ONE_HEALTH , 8000: $WAS_TWO_HEALTH"

if [ $WAS_ONE_8080_HEALTH -eq "UP" -a $WAS_ONE_8000_HEALTH -ne "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8080입니다."
        IDLE_PORT_ONE=8000
elif [ $WAS_ONE_8080_HEALTH -ne "UP" -a $WAS_ONE_8000_HEALTH -eq "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8000입니다."
        IDLE_PORT_ONE=8080
else
        echo "> 현재 구동 중인 애플리케이션의 포트를 찾는데 실패하였습니다."
        echo "> 8080 포트를 할당합니다."
        IDLE_PORT_ONE=8080
fi

echo "> 현재 구동중인 PORT 상태 확인"
WAS_TWO_8080_HEALTH=$(curl -s http://54.180.158.203:8080/infra/health)
WAS_TWO_8000_HEALTH=$(curl -s http://54.180.158.203:8000/infra/health)
echo "> 8080: $WAS_ONE_HEALTH , 8000: $WAS_TWO_HEALTH"

if [ $WAS_TWO_8080_HEALTH -eq "UP" -a $WAS_TWO_8000_HEALTH -ne "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8080입니다."
        IDLE_PORT_TWO=8000
elif [ $WAS_TWO_8080_HEALTH -ne "UP" -a $WAS_TWO_8000_HEALTH -eq "UP" ]; then
        echo "> 현재 구동 중인 애플리케이션의 포트는 8000입니다."
        IDLE_PORT_TWO=8080
else
        echo "> 현재 구동 중인 애플리케이션의 포트를 찾는데 실패하였습니다."
        echo "> 8080 포트를 할당합니다."
        IDLE_PORT_TWO=8080
fi

echo "> 전환할 WAS 1 Port: $IDLE_PORT_ONE"
echo "> Port 전환"
echo "set \$service_port_one ${IDLE_PORT_ONE};" |sudo tee /etc/nginx/conf.d/service-url.inc

echo "> 전환할 WAS 2 Port: $IDLE_PORT_TWO"
echo "> Port 전환"
echo "set \$service_port_two ${IDLE_PORT_TWO};" |sudo tee /etc/nginx/conf.d/service-url.inc

이 방법으로는 무중단 배포를 하기엔 많은 어려움이 있다는 사실을 적으면서 깨달았기 때문에 ㅎ
일단 다음과 같은 문제점들이 있다. 

1-4번 과정이 성공적으로 끝난다고 가정하면, 각각의 was에서 기존 port를 kill해야하는데 이 시점에서 두개의 port가 살아있는데 어떤 port가 신규 port인지 nginx가 있는 서버에서는 알 수 없다. 때문에 기존 port를 죽일 마땅한 방법이 없다. 이런채로 5번 과정을 진행하지 못한다면 다음 배포 시 2번 과정을 진행할 수 없다. (일단 생각나는대로 주절주절 적어봤는데, 생각이 정리되면 다시 수정할 예정)

로드 밸런싱, 블루-그린 방식의 무중단 배포 구상

몇가지 아이디어를 얻어서, 최종적으로 구상하는 흐름은 다음과 같다.

기존에 있던 nginx 웹서버와 바로 was를 연결하는 방식이 아닌, was 인스턴스에 nginx를 하나 더 두게 된다. 
이 nginx는 무중단 배포를 위한 프록시 역할을 한다.
포트를 변경할 nginx와 was가 한 인스턴스 안에 있기 때문에, 한 스크립트로 nginx 설정 변경과 빌드, 기존 포트 kill이 한 사이클에 동작할 수 있다. 

좀 더 자세한 흐름을 살펴보자.

먼저 젠킨스에서 각각의 was 인스턴스에 다음과 같은 작업이 진행된다.

  1. 빌드한 jar 파일 ssh로 인스턴스에 전송 
  2. 해당 인스턴스에 접속해 인스턴스에 있는 deploy.sh을 실행시킨다.

deploy.sh의 로직은 다음과 같다. 

  1. 사용하고 있는 기존 port를 "/infra/port"로 확인
  2. (사용하고 있는) 기존 port의 pid 확인 `sudo netstat -ntlp | grep :$CURRENT_PORT | awk '{print $7}' | tr -cd [0-9]`
  3. (사용하지 않는) 신규 port로 애플리케이션 배포 
  4. 배포 10초 후, 신규 port로 "/infra/health"를 통해 헬스 체크
  5. nginx가 포워딩하고 있는 포트 변경
    `echo "set \$service_port_one ${IDLE_PORT_ONE};" |sudo tee /etc/nginx/conf.d/service-url.inc`
  6. nginx 리로드
  7. 기존 port를 kill

1. 사용하고 있는 기존 port를 "/infra/port"로 확인

애플리케이션에 현재 애플리케이션이 실행되고 있는 port와 health 체크용 api를 만들었다. 

public class InfraController {

    private final Environment env;

    @GetMapping("/health")
    public String health() {
        return "UP";
    }

    @GetMapping("/port")
    public String port() {
        return env.getProperty("local.server.port");
    }
}

해당 api를 사용해 현재 nginx로 포트포워딩이 되어 있어 서비스되고 있는 기본 port를 알아낸다.

CURRENT_PORT=$(curl -s http://127.0.0.1/infra/port)
echo "> 현재 구동중인 PORT : $CURRENT_PORT"

2. 기존 port의 pid 확인

사용하고 있던 기존 port를 알아냈다면 해당 포트가 실행되고 있는 pid를 알아낸다. 
또한 기존 port와 반대되는 배포 대상이 될 신규 port도 변수에 할당한다.

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)
echo "> 현재 구동중인 애플리케이션 PID : $CURRENT_PID"

if [ $CURRENT_PORT == "8080" ]; then
        echo "> 기존 애플리케이션의 포트는 8080입니다."
        TARGET_PORT=8000
elif [ $CURRENT_PORT == "8000" ]; then
        echo "> 기존 애플리케이션의 포트는 8000입니다."
        TARGET_PORT=8080
else
        echo "> 현재 구동 중인 애플리케이션의 포트를 찾는데 실패하였습니다."
        echo "> 8080 포트를 할당합니다."
        TARGET_PORT=8080
fi

3. 신규 port로 애플리케이션 배포 

배포가 진행되면서도 2개의 서버가 서비스를 유지해야한다. 
때문에 기존 port를 kill 하기 전, 새로운 서버의 배포과정을 진행한다.

echo "> 새 애플리케이션 배포 - 포트번호 : $TARGET_PORT"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=local -Dserver.port=$TARGET_PORT $REPOSITORY/$JAR_NAME 1> log.txt 2>&1 &

4. 배포 10초 후, 신규 port로 "/infra/health"를 통해 헬스 체크

신규 port로 띄워진 서버의 헬스체크를 진행한다. 
애플리케이션의 실행에 시간이 걸릴 수 있으므로 10초 후부터 헬스체크를 시행한다. 

echo "> $TARGET_PORT 10초 후 Health check 시작"
echo "> curl -s http://localhost:$TARGET_PORT/infra/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$TARGET_PORT/infra/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      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

5. nginx가 포워딩하고 있는 포트 변경

그 전에 잠깐, nginx에서 해주어야 할 설정이 있다.
우리는 동적으로 nginx가 포워딩할 서버의 port를 변경해 줄 것인데, 
reload하는 시점 외부 변수를 읽어 port를 할당하기 위해 외부 파일을 생성한다. 

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

 

nignx.conf에서는 다음과 같이 외부 설정을 include해 올 수 있다.

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

      proxy_pass $service_port;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
    }

신규 port로 배포한 was가 정상적으로 동작한다면,
현재 80포트로 들어오는 요청에 대해 한 port를 연결해주고 있는데, 이 서비스 가능한 port를 신규 port로 변경해준다. 

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

6. nginx 리로드

변경 사항(nginx.conf)을 적용하기 위해 nginx를 재구동 시킨다. 

echo "> Nginx Reload"
sudo service nginx reload

7. 기존 port를 kill

모든 과정이 정상적으로 진행되었다면, 신규 port가 배포되었으니 불필요한 기존 port는 죽이자. 

잘가라 !

echo "기존 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID

        while kill -0 $CURRENT_PID; do
                sleep 5
                echo "> 프로세스가 아직 종료되지 않았습니다."
        done
        echo "> $CURRENT_PID 종료 완료"
fi

모든 과정이 끝났다.
아직 nolto에는 (데모가 코앞이기 때문에) 적용은 못했고, 후에 차차 도입할 예정이다. 
완성된 스크립트들은 다음과 같다.

deploy.sh 파일

REPOSITORY=/home/ubuntu/nolto2021
PROJECT=nolto

cd $REPOSITORY/

echo "> application 디렉터리로 이동"

# 1
CURRENT_PORT=$(curl -s http://127.0.0.1/infra/port)
echo "> 현재 구동중인 PORT : $CURRENT_PORT"

# 2
CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)
echo "> 현재 구동중인 애플리케이션 PID : $CURRENT_PID"

if [ $CURRENT_PORT == "8080" ]; then
        echo "> 기존 애플리케이션의 포트는 8080입니다."
        TARGET_PORT=8000
elif [ $CURRENT_PORT == "8000" ]; then
        echo "> 기존 애플리케이션의 포트는 8000입니다."
        TARGET_PORT=8080
else
        echo "> 현재 구동 중인 애플리케이션의 포트를 찾는데 실패하였습니다."
        echo "> 8080 포트를 할당합니다."
        TARGET_PORT=8080
fi

# 3
echo "> 새 애플리케이션 배포 - 포트번호 : $TARGET_PORT"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=local -Dserver.port=$TARGET_PORT $REPOSITORY/$JAR_NAME 1> log.txt 2>&1 &

# 4
echo "> $TARGET_PORT 10초 후 Health check 시작"
echo "> curl -s http://localhost:$TARGET_PORT/infra/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$TARGET_PORT/infra/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      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

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

# 6
echo "> Nginx Reload"
sudo service nginx reload

PROXY_PORT=$(curl -s http://localhost/infra/port)
echo "> Nginx Current Proxy Port: $PROXY_PORT"

# 7
echo "기존 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID

        while kill -0 $CURRENT_PID; do
                sleep 5
                echo "> 프로세스가 아직 종료되지 않았습니다."
        done
        echo "> $CURRENT_PID 종료 완료"
fi

nginx.conf

events {}

http {
  // ...

  server {
    listen 80;

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

      proxy_pass $service_port;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
    }
  }
}

참고자료

7) 스프링부트로 웹 서비스 출시하기 - 7. Nginx를 활용한 무중단 배포 구축하기

'인프라' 카테고리의 다른 글

Saga 패턴  (0) 2022.09.18
Grafana Loki와 LogQL 에 대해 알아보기  (0) 2022.07.24
AWS CloudFront와 S3 구성  (0) 2021.08.23
생활코딩 OAuth 2.0 강의와 놀토의 OAuth  (0) 2021.08.20
Flyway 적용기  (0) 2021.08.15