본문 바로가기

배포

NGINX 무중단 배포

* "스프링 부트와 AWS로 혼자 구현하는 웹 서비스 " 책을 보고 저에게 맞게 적용한 내용을 정리하였습니다.

1. 설정 파일

  • /etc/nginx/nginx.conf
    • proxy_pass : 80 포트로 연결할 주소 및 포트 (ex)localhost:8081)
    • proxy_set_header : 80 포트의 헤더의 요청값을 그대로 가져와 연결
    • include : service_url 변수의 값을 할당 받기 위해 import
  • 1
    2
    3
    4
    5
    6
    7
    include /etc/nginx/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;
        }
    cs
  • /etc/nginx/conf.d/service-url.inc
    1
    set $service_url http://127.0.0.1:8082;
    cs

2. Shell Script 파일

  • deploy.sh
    1
    2
    3
    4
    #!/usr/bin/env bash
    sh script/stop.sh
    sh script/start.sh
    sh script/health.sh
    cs
    • #!/usr/bin/env bash : 실행 시에 bash 를 쓴다는 뜻
    • 배포 시에 명령어로 실행하는 스크립트
  • profile.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #!/usr/bin/env bash
     
    function find_idle_profile()
    {
        RESPONSE_CODE=$(curl --/dev/null -"%{http_code}" http://localhost/profile)
     
        if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함)
        then
            CURRENT_PROFILE=dev2
        else
            CURRENT_PROFILE=$(curl -s http://localhost/profile)
        fi
     
        if [ ${CURRENT_PROFILE} == dev1 ]
        then
          IDLE_PROFILE=dev2
        else
          IDLE_PROFILE=dev1
        fi
     
        echo "${IDLE_PROFILE}"
    }
     
     
    function find_idle_port()
    {
        IDLE_PROFILE=$(find_idle_profile)
     
        if [ ${IDLE_PROFILE} == dev1 ]
        then
          echo "8081"
        else
          echo "8082"
        fi
    }
    cs
    • bash는 return value가 안되니 제일 마지막 줄에 echo로 해서 결과 출력후, 클라이언트에서 값을 사용한다.
    • find_idle_profile() : 쉬고 있는 profile 찾기: dev1이 사용중 이면 dev2가 쉬고 있고, 반대면 dev1이 쉬고 있음
    • find_idle_port(): 쉬고 있는 profile의 port 찾기
  • switch.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #!/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 restart
    }
    cs
    • switch_proxy : 쉬고 있는 포트를 80 포트로 연결
  • stop.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #!/usr/bin/env bash
     
    ABSDIR=$(pwd)/script
    echo $ABSDIR
     
    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 5
    fi
    cs
    • 현재 80에 포워딩 되지 않은 포트 pid 검색 및 종료
  • start.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #!/usr/bin/env bash
     
    ABSPATH=$(readlink -f $0)
    ABSDIR=$(dirname $ABSPATH)
    source ${ABSDIR}/profile.sh
     
    REPOSITORY=$(pwd)
    PROJECT_NAME=Myproject
     
    IDLE_PROFILE=$(find_idle_profile)
     
    echo "> Build 파일 복사"
     
    ./gradlew clean build -Pprofile=$IDLE_PROFILE
    echo ">./gradlew clean build -Pprofile=$IDLE_PROFILE"
     
    echo "> 새 어플리케이션 배포"
    JAR_NAME=$(ls -tr $REPOSITORY/build/libs/*.war | tail -1)
     
    echo "> JAR Name: $JAR_NAME"
    sudo chmod +x $JAR_NAME
     
    echo "> $JAR_NAME 에 실행권한 추가"
     
     
    echo "> $JAR_NAME 실행"
     
     
    echo ${REPOSITORY}
    echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."
     
    nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE 
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
    echo "> cat  $REPOSITORY/nohup.out"
    echo "end"
    cs

     

    • 다음 배포할 profile을 받고 빌드
    • war 파일에 실행 권한을 주고 실행
    • nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $JAR_NAME > $REPOSITORY/nohup.out 2>&1 & : war파일 실행 명령어
      1. nohup : 터미널 세션이 끝나도 계속 실행
      2. java -jar $JAR_NAME: $JAR_NAME .war 파일 실행
      3. -Dspring.profiles.active=$IDLE_PROFILE : spring profile을 IDLE_PROFILE로 실행
      4. $REPOSITORY/nohup.out : stdout 로그를 nohup.out 파일에 기록
      5. 2>&1 : stderr도 stdout에 함께 기록(모두 nohup.out 파일에 기록됨)
      6. & : 백그라운드로 실행
  • health.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #!/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 -X GET http://127.0.0.1:$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 'dev' | wc -l)
     
      if [ ${UP_COUNT} -ge 1 ]
      then # $up_count >= 1 ("dev" 문자열이 있는지 검증)
          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
    cs
    • 새로 배포된 포트에 profile 과 정상 응답이 오는지 체크
    • 10번 반복될 동안 모두 실패한다면 원래 80에 연결 되었는 포트에 배포

3. profile check api

  • {root}/profile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.Arrays;
import java.util.List;
 
@RestController
@RequiredArgsConstructor
public class EnvController {
    private final Environment environment;
 
    @GetMapping("/profile")
    public String profile(){
        List<String> profiles = Arrays.asList(environment.getActiveProfiles());
 
        List<String> activeProfiles = Arrays.asList("dev1","dev2","prod1","prod2");
        System.out.println(profiles);
        System.out.println(activeProfiles);
        String defaultProfile = "dev";
        System.out.println(defaultProfile);
        return profiles.stream().filter(activeProfiles::contains).findAny().orElse("ASDF");
    }
}
cs
  • environment에서 현재 적용된 profile 목록을 모두 가져옴
  • "dev1","dev2","prod1","prod2" 중 포함된 값을 return;
  • 아무것도 없으면 ASDF(default로 설정하지 않은 것은 빠른 체크를 위해)
  • dev는 개발서버 / prod는 실서버