배포 스크립트 작성 및 Nginx 설정

이번 글에서는 무중단 배포를 3가지 스탭으로 나눠서 배포용 쉘 스크립트를 작성하고,

CodeDeploy에서 배포를 위해 참고할 AppSpec 파일을 작성할 것이다.

배포 스크립트는 아래와 같이 세가지 스탭으로 나눈다.

  1. 서비스 중인 WAS와 다른 포트에 새로운 버전의 WAS를 띄운다.
  2. 1번의 과정이 정상 동작 했는지 해당 포트로 HTTP 요청을 보내 확인한다.
  3. 1, 2가 정상 동작한다면 Nginx가 새로운 버전의 WAS를 바라보도록 변경하고 기존의 WAS를 종료한다.

1. 배포 스크립트 작성

이제 배포 시 실행할 쉘 스크립트를 작성할 것이다.

기존에 리버스 프록시로 이용하고 있던 Nginx를 여기서도 써먹어서 무중단 배포를 구현한다.

배포 스크립트의 각 스탭에 주석을 달아두었으니 리눅스 커맨드를 어느정도 알고 있다면 쉽게 이해할 수 있을 것이다.

1-1. run_new_was

업데이트 된 버전의 새로운 WAS를 띄운다.

# run_new_was.sh

#!/bin/bash

# 환경 변수 설정
PROJECT_ROOT="/home/ubuntu/app" # 프로젝트 루트
JAR_FILE="$PROJECT_ROOT/build/libs/team-0.0.1-SNAPSHOT.jar" # 빌드해서 생성된 jar 파일명

# service_url.inc 에서 현재 서비스 중인 WAS의 포트 번호 확인
CURRENT_PORT=$(cat /home/ubuntu/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0

echo "> Current port of running WAS is ${CURRENT_PORT}."

# 서비스 중인 포트가 8081이면 8082 포트로 배포
# 서비스 중인 포트가 8082이면 8081 포트로 배포
if [ ${CURRENT_PORT} -eq 8081 ]; then
  TARGET_PORT=8082
elif [ ${CURRENT_PORT} -eq 8082 ]; then
  TARGET_PORT=8081
else
  echo "> Any WAS is connected to nginx" # 애플리케이션이 실행되고 있지 않음
fi

# 타겟 포트 번호로 실행 중인 프로세스가 있는지 확인
TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')

# PID를 이용해 타겟 포트로 실행 중인 프로세스 Kill
if [ ! -z ${TARGET_PID} ]; then
  echo "> Kill ${TARGET_PORT}."
  sudo kill ${TARGET_PID}
fi

# 타켓 포트로 업데이트 된 버전의 새로운 서버 실행
nohup java -jar -Dserver.port=${TARGET_PORT} ${JAR_FILE} > /home/ubuntu/nohup.out 2>&1 &
echo "> Now new WAS runs at ${TARGET_PORT}."
exit 0

1-2. health_check

새로 띄운 WAS가 정상 동작하는지 헬스 체크한다.

# health_check.sh

#!/bin/bash

# 환경 변수 설정
# service_url.inc에서 현재 서비스 중인 WAS의 포트 번호 확인
# 해당 포트 번호로 health_check 실행
CURRENT_PORT=$(cat /home/ubuntu/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0

if [ ${CURRENT_PORT} -eq 8081 ]; then
    TARGET_PORT=8082
elif [ ${CURRENT_PORT} -eq 8082 ]; then
    TARGET_PORT=8081
else
    echo "> Any WAS is connected to nginx"  # 헬스체크 시 Nginx에 어떤 WAS도 연결돼있지 않으면 에러 코드
    exit 1
fi

echo "> Start health check of WAS at 'http://127.0.0.1:${TARGET_PORT}' ..."

# "/" 경로로 헬스 체크하여 응답 코드를 보고 서버가 정상적으로 작동하는지 확인
# 최대 10번까지 테스트 해서 그 안에 성공하면 통과(WAS가 늦게 뜨는 경우를 대비한 안전 장치)
for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10
do
    echo "> #${RETRY_COUNT} trying..."
    # 테스트할 API 주소를 통해 http 상태 코드 확인
    RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${TARGET_PORT})

	  # RESPONSE_CODE의 http 상태가 200번인 경우 성공
    if [ ${RESPONSE_CODE} -eq 200 ]; then
        echo "> New WAS successfully running"
        exit 0
    elif [ ${RETRY_COUNT} -eq 10 ]; then
        echo "> Health check failed."
        exit 1
    fi
    sleep 5  # 각 시도 마다 5초간 대기
done

 

이 때, 본인은 "/"  경로로 헬스 체크를 수행하였다.

따라서 "/" 경로로 GET 요청 시 상태코드 200을 반환하는 컨트롤러를 하나 만들어줘야 한다.

포트번호까지 확인하고 싶어서 아래처럼 해주었다.

@Slf4j
@RestController
public class HealthChecktController implements ApplicationListener<WebServerInitializedEvent> {

    private int serverPort;

    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        this.serverPort = event.getWebServer().getPort();
    }

    @GetMapping
    public String health() {
        log.info("[헬스체크] 포트번호: {} 호출", serverPort);
        return "hi" + serverPort;
    }
}

1-3. switch

위 두 단계가 정상적으로 진행됐다면 Nginx가 바라보는 애플리케이션을 스위칭한다.

그리고 이전 버전의 WAS는 종료한다.

# switch.sh

#!/bin/bash

# service_url.inc에서 현재 서비스 중인 WAS의 포트 번호 확인
CURRENT_PORT=$(cat /home/ubuntu/service_url.inc  | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0

echo "> Nginx currently proxies to ${CURRENT_PORT}."

if [ ${CURRENT_PORT} -eq 8081 ]; then
    TARGET_PORT=8082
elif [ ${CURRENT_PORT} -eq 8082 ]; then
    TARGET_PORT=8081
else
    echo "> No WAS is connected to nginx"
    exit 1
fi

# service_url.inc 파일에 적힌 서비스 주소를 새로 띄운 서버의 주소로 변경
echo "set \$service_url http://127.0.0.1:${TARGET_PORT};" | tee /home/ubuntu/service_url.inc

echo "> Now Nginx proxies to ${TARGET_PORT}."

# service_url이 변경됐으므로 nginx를 reload 해줌
sudo service nginx reload

echo "> Nginx reloaded."

# -9 SIGKILL 은 서버를 바로 종료하므로
# -15 SIGTERM  안전 종료인 SIGTERM을 사용하여 이전 포트 프로세스를 제거한다.
CURRENT_PID=$(lsof -Fp -i TCP:${CURRENT_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')

if [ -z "$CURRENT_PID" ]; then
    echo "> No process found on port ${CURRENT_PORT}."
else
    echo "> Killing process ${CURRENT_PID} on port ${CURRENT_PORT}."
    sudo kill -15 ${CURRENT_PID}

    # kill 명령이 실패했는지 확인
    if [ $? -eq 0 ]; then
        echo "> Process ${CURRENT_PID} successfully terminated."
    else
        echo "> Failed to terminate process ${CURRENT_PID}."
        exit 1
    fi
fi

2. 무중단 배포를 위한 Nginx 설정

8081, 8082 두개의 포트를 이용하여 애플리케이션을 띄우는 방식으로 무중단 배포를 구현한다.

기존 서비스가 8081이면 8082 포트로 새로운 버전의 애플리케이션을 띄우고, 정상 동작 시 기존 8081 포트를 내린다.

Nginx로 도메인으로의 요청을 포트포워딩 해주고 있으므로 Nginx 설정을 배포 시에 자동으로 수정하도록 변경해줘야 한다.

 

1에서 배포 스크립트를 작성하면서 service_url.inc 를 보았을 것이다.

service_url.inc의 내용은 아래와 같다.

# service_url.inc
set $service_url http://127.0.0.1:8081;

 

배포 시 매번 $service_url이 변경되면서 Nginx가 바라볼 포트 번호를 지정해준다.

/etc/nginx/sites-available/default 파일이 service_url.inc의 $service_url를 이용할 수 있도록 아래처럼 수정해주자.

include /home/ubuntu/service_url.inc; 구문으로 /home/ubuntu/service_url.inc 를 참조할 수 있게 해준다.

다른 포트로 접근 시 막히거나 리다이렉트 되기 때문에 443 포트로 접근할 때만 설정해주면 된다.


3. AppSpec 파일 작성

CodeDeploy Agent는 배포 시 appspec.yml 파일을 참고하여 배포 프로세스를 진행한다.

(참고로 파일 이름은 꼭 appspec.yml 이어야 한다!)

 

본인은 EC2의 /home/ubuntu/app 폴더에서 배포를 진행할 것이다.

아래처럼 설정하면 CodeDeploy Agent가 동작할 때 S3 버킷의 zip 파일을 /home/ubuntu/app 로 압축해제한다.

AppSpec 파일은 기본적으로 배포를 실행할 폴더의 루트 디렉터리 에 위치해야 하기 때문에

압축 시 루트 디렉터리에 위치시켜주면 된다.

appspec.yml

files의 overwrite: yes 로 설정하면 /home/ubunut/app 경로가 없을 시 자동으로 생성한다.

해당 옵션이 없을 때 /home/ubunut/app 경로가 없으면 배포에 실패하므로 참고하자.

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/app
    overwrite: yes  # /home/ubuntu/app 경로가 없을 시 생성함

permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

hooks:
  ApplicationStart:
    - location: scripts/run_new_was.sh
      timeout: 60
      runas: ubuntu
    - location: scripts/health_check.sh
      timeout: 60
      runas: ubuntu
    - location: scripts/switch.sh
      timeout: 60
      runas: ubuntu

+ Recent posts