본문 바로가기
Dev/AWS

Travis CI,CodeDeploy,S3,Nginx로 EC2에 무중단 배포하기(3) - Nginx로 무중단 배포 하기

by ssyoni 2022. 4. 11.
반응형

https://ssyoni.tistory.com/25

 

Travis CI,CodeDeploy,S3,Nginx로 EC2에 무중단 배포하기(1) - Travis CI,S3 연동

스프링부트와 JPA를 사용한 토이 프로젝트를 진행하면서 CI/CD 구축 과정을 정리한 글입니다. Travis CI를 활용해 프로젝트를 빌드하고, AWS S3와 AWS CodeDeploy, Nginx를 통해 EC2 서버에 무중단으로 배포하

ssyoni.tistory.com

https://ssyoni.tistory.com/26

 

Travis CI,CodeDeploy,S3,Nginx로 EC2에 무중단 배포하기(2) - Travis CI,S3,CodeDeploy 연동

CodeDeploy를 생성하고 S3로부터 받은 파일을 EC2 서버로 배포하는 과정을 정리하였습니다. 설명하기에 앞서 요약을 해보자면 EC2와 CodeDeploy 서비스가 서로 소통(?)할 수 있게 각각 서비스에 대한 권

ssyoni.tistory.com

이전 포스팅에서 Travis CI로 소스를 빌드하고 S3, AWS CodeDeploy를 통해 빌드한 파일을 EC2 서버로 전송하는 과정까지를 진행하였습니다. EC2에 전송한 JAR파일을 실행시키려면 현재 구동 중인 프로젝트를 중지하고 다시 기동 해야 하는데 이렇게 배포 과정에서 서비스를 중지해야 한다면, 배포 시 오류 발생 상황에 대처하기 힘들어지고 서비스 사용자 입장에서도 서버가 중지되어 불편함을 겪게 됩니다. 

이러한 상황을 대비해 서비스를 중지하지 않고 배포할 수 있는 무중단 배포환경 구축할 수 있습니다. 무중단 배포 방식에는 AWS Blue Green, Docker를 이용한 무중단 배포, L4 스위치를 이용한 무중단 배포 등 여러가지가 있습니다. 이번 포스팅에서는 오픈소스 소프트웨어인 Nginx를 활용한 무중단 배포 환경을 구축하는 과정을 정리하였습니다. 

 

1. nginx 설치 

아마존 리눅스2에서는 yum을 통한 nginx 설치를 지원하지 않습니다. 

아래의 명령어를 통해 nginx 설치 패키지를 조회하고 설치를 진행합니다. 

$ amazon-linux-extras list | grep nginx

조회한 nginx 설치를 진행합니다. 

$ sudo amazon-linux-extras install -y nginx1

nginx 설치 확인 

$ nginx -v

nginx 설치를 완료한 뒤 서비스를 실행시킵니다. 

$ sudo service nginx start

 

 

2. AWS 보안그룹 추가 -> 인바운드 규칙에 80 포트 개방 

nginx로 접속 시 80포트로 접속하기 때문에 aws 보안 그룹 인바운드 규칙에 80 코드를 개방시켜주어야 합니다. 

 

3. nginx <-> Springboot 연동 

nginx가 현재 구동중인 스프링 부트를 바라보도록 프록시 설정을 해주어야 합니다. 

먼저 nginx 설정들이 모여있는 /etc/nginx/conf.d 에 service-url.inc 파일을 생성합니다. 

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

 

그다음 nginx.conf 파일을 설정해줍니다. 위에서 생성한 service-url.inc파일을 include 해주고 server 섹션의 location 부분에다가 proxy_pass를 설정해줍니다. 

sudo vim /etc/nginx/nginx.conf
   server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        # include /etc/nginx/default.d/*.conf;
        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;
        }

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }

proxy_pass : nginx 요청이 오면 service-url에 설정한 url로 전달합니다.

 

수정을 마친 뒤 nginx 서비스를 재기동해주고 포트 번호 없이 브라우저로 접속하면 nginx 프록시 접속이 잘 되는 것을 확인할 수 있습니다. 

sudo service nginx restart

 

 

4. 무중단 배포 환경 구성

무중단 배포를 위한 profile을 2개 추가해줍니다 

profile는하나의 프로젝트 코드를 여러 개의 환경으로 실행시킬 수 있게 해주는 설정 기능입니다. 8081, 8082 포트로 접속할 수 있는 설정 파일(real1, real2)을 각각 생성하면 후에 실행하고 싶은 환경으로 프로젝트 코드를 실행시킬 수 있습니다.

 

application-real1.properties

server.port=8081
...

application-real1.properties

server.port=8082
...

 

API 추가 (ProfileController)

프로젝트에 현재 구동중인 환경 정보를 가져와 구분하는 API를 추가해줍니다. 나중에 배포 후 real1 환경으로 접속 시 "real1"값을, real2 환경으로 접속 시 "real2"값을 화면에 반환합니다.

 

ProfileController.class

@RestController
@RequiredArgsConstructor
public class ProfileController {

    private final Environment env;

    @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);
    }

}

Environment : 스프링부트 환경설정에 대한 정보를 제공하는 인터페이스입니다. Environment 인터페이스를 통해서 profiles에 접근할 수 있습니다. 참고로 설정값 변경은 불가능합니다. 

getActiveProfiles() : 현재 활성화 된 profile을 String 값으로 반환합니다. 

 

 

EC2에 무중단 배포 디렉토리 생성 

무중단 배포 시 파일을 업로드 할 디렉터리를 EC2환경에 생성해줍니다. 

mkdir ~/app/step3 && mkdir ~/app/step3/zip

 

appspec.yml 배포 경로 수정 

appsecc.yml 파일에서 위에서 생성한 디렉토리(step3)로 배포 대상 경로를 수정해줍니다. 

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step4/zip
    overwrite: yes

 

무중단 배포 스크립트 생성 

프로젝트에 scripts 디렉토리 하위에 무중단 배포를 위한 스크립트 파일을 생성해줍니다. 

- stop.sh : 실행중인 스프링 부트 종료

- start.sh : 새로 배포할 스프링부트를 실행 중이지 않은 profile로 실행

- health.sh : start.sh로 실행시킨 프로젝트의 상태 체크 

- switch.sh : 엔진엑스가 바라보는 스프링부트를 최신 버전으로 변경 

- profile.sh : 포트 체크 로직. 위의 4개의 스크립트 파일에서 임포트 하여 사용한다. 

 

stop.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH) # stop.sh 가 속한 경로 찾기. -> profile.sh의 경로를 찾기 위해 사용
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

 

start.sh

#!/usr/bin/env bash

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

REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=miniproject

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.yml,classpath:/application-$IDLE_PROFILE.properties \
    -Dspring.profiles.active=$IDLE_PROFILE \
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

 

health.sh

#!/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

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

 

profile.sh

#!/usr/bin/env bash

# 쉬고 있는 profile 찾기: real1이 사용 중이면 real2가 쉬고 있고, 반대면 real1이 쉬고 있음
function find_idle_profile()
{
    RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)  # nginx가 바라보고 있는 스프링 부트가 정상적으로 수행중인지 확인

    if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함) / 정상 : 200, 오류 400~503
    then
        CURRENT_PROFILE=real2
    else
        CURRENT_PROFILE=$(curl -s http://localhost/profile)
    fi

    if [ ${CURRENT_PROFILE} == real1 ]
    then # IDLE_PROFILE : nginx와 연결되지 않은 profile
      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
}

 

 

appspec.yml 스크립트 실행 설정 추가 

appspec.yml 의 hooks 세션에 위에서 생성한 스크립트를 실행하는 이벤트들을 설정합니다. 

참고로 appspec.yml에서 hooks 세션은 해당 배포 컴퓨팅 환경에 따라서 역할이 달라집니다. EC2/온프레미스 환경에서의 hooks 세션은 배포의 수명주기나 하나 이상의 스크립트 실행을 연결시키는 매핑을 포함합니다. 

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step3/zip
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

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

hooks 섹션 부분은 Jar파일이 복사된 이후부터 차례대로 실행됩니다.

 

 

5. 무중단 배포 테스트 

 

CodeDeploy 로그 확인 

tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
[2022-04-01 13:31:57.314] [d-OET4S1CWF]LifecycleEvent - AfterInstall
[2022-04-01 13:31:57.314] [d-OET4S1CWF]Script - stop.sh
[2022-04-01 13:31:57.343] [d-OET4S1CWF][stdout]> 8081 에서 구동중인 애플리케이션 pid 확인
[2022-04-01 13:31:57.388] [d-OET4S1CWF][stderr]/opt/codedeploy-agent/deployment-root/8364edf8-f041-4472-8f42-dffbbdb0388d/d-OET4S1CWF/deployment-archive/stop.sh: line 17: kill: 21441됨: arguments must be process or job IDs
[2022-04-01 13:31:57.388] [d-OET4S1CWF][stdout]> kill -15 21441됨
[2022-04-01 13:32:02.555] [d-OET4S1CWF]LifecycleEvent - ApplicationStart
[2022-04-01 13:32:02.555] [d-OET4S1CWF]Script - start.sh
[2022-04-01 13:32:02.570] [d-OET4S1CWF][stdout]> Build 파일 복사
[2022-04-01 13:32:02.570] [d-OET4S1CWF][stdout]> cp /home/ec2-user/app/step3/zip/*.jar /home/ec2-user/app/step3/
[2022-04-01 13:32:02.583] [d-OET4S1CWF][stdout]> 새 어플리케이션 배포
[2022-04-01 13:32:02.586] [d-OET4S1CWF][stdout]> JAR Name: /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar
[2022-04-01 13:32:02.586] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 에 실행권한 추가
[2022-04-01 13:32:02.587] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 실행
[2022-04-01 13:32:02.598] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 를 profile=real1 로 실행합니다.
[2022-04-01 13:32:03.611] [d-OET4S1CWF]LifecycleEvent - ValidateService
[2022-04-01 13:32:03.611] [d-OET4S1CWF]Script - health.sh
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> Health Check Start!
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> IDLE_PORT: 8081
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> curl -s http://localhost:8081/profile
[2022-04-01 13:32:13.765] [d-OET4S1CWF][stdout]> Health check 성공
[2022-04-01 13:32:13.777] [d-OET4S1CWF][stdout]> 전환할 Port: 8081
[2022-04-01 13:32:13.777] [d-OET4S1CWF][stdout]> Port 전환
[2022-04-01 13:32:13.788] [d-OET4S1CWF][stdout]set $service_url http://127.0.0.1:8081;
[2022-04-01 13:32:13.790] [d-OET4S1CWF][stdout]> 엔진엑스 Reload
[2022-04-01 13:32:13.811] [d-OET4S1CWF][stderr]Redirecting to /bin/systemctl reload nginx.service
[2022-04-01 13:38:22.877] [d-8TE4ATCWF]LifecycleEvent - AfterInstall
[2022-04-01 13:38:22.877] [d-8TE4ATCWF]Script - stop.sh
[2022-04-01 13:38:22.914] [d-8TE4ATCWF][stdout]> 8082 에서 구동중인 애플리케이션 pid 확인
[2022-04-01 13:38:22.962] [d-8TE4ATCWF][stderr]/opt/codedeploy-agent/deployment-root/8364edf8-f041-4472-8f42-dffbbdb0388d/d-8TE4ATCWF/deployment-archive/stop.sh: line 17: kill: 됨: arguments must be process or job IDs
[2022-04-01 13:38:22.962] [d-8TE4ATCWF][stdout]> kill -15 됨
[2022-04-01 13:38:28.145] [d-8TE4ATCWF]LifecycleEvent - ApplicationStart
[2022-04-01 13:38:28.145] [d-8TE4ATCWF]Script - start.sh
[2022-04-01 13:38:28.160] [d-8TE4ATCWF][stdout]> Build 파일 복사
[2022-04-01 13:38:28.160] [d-8TE4ATCWF][stdout]> cp /home/ec2-user/app/step3/zip/*.jar /home/ec2-user/app/step3/
[2022-04-01 13:38:28.172] [d-8TE4ATCWF][stdout]> 새 어플리케이션 배포
[2022-04-01 13:38:28.175] [d-8TE4ATCWF][stdout]> JAR Name: /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar
[2022-04-01 13:38:28.175] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 에 실행권한 추가
[2022-04-01 13:38:28.176] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 실행
[2022-04-01 13:38:28.194] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 를 profile=real2 로 실행합니다.
[2022-04-01 13:38:29.211] [d-8TE4ATCWF]LifecycleEvent - ValidateService
[2022-04-01 13:38:29.211] [d-8TE4ATCWF]Script - health.sh
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> Health Check Start!
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> IDLE_PORT: 8082
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> curl -s http://localhost:8082/profile
[2022-04-01 13:38:39.626] [d-8TE4ATCWF][stdout]> Health check 성공
[2022-04-01 13:38:39.673] [d-8TE4ATCWF][stdout]> 전환할 Port: 8082
[2022-04-01 13:38:39.673] [d-8TE4ATCWF][stdout]> Port 전환
[2022-04-01 13:38:39.695] [d-8TE4ATCWF][stdout]set $service_url http://127.0.0.1:8082;
[2022-04-01 13:38:39.697] [d-8TE4ATCWF][stdout]> 엔진엑스 Reload
[2022-04-01 13:38:39.720] [d-8TE4ATCWF][stderr]Redirecting to /bin/systemctl reload nginx.service

 

자바 어플리케이션 실행 여부 확인 

ps -ef | grep java

아래와 같이 2개의 어플리케이션이 실행되는 것을 확인할 수 있습니다. 

java -jar -Dspring.config.location=...-Dspring.profiles.active=real1 /home/ec2-user/app/step3/~~.jar
java -jar -Dspring.config.location=...-Dspring.profiles.active=real2 /home/ec2-user/app/step3/~~.jar

 

 

무중단 배포 성공!

 

 

 

 

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/Environment.html

 

Environment (Spring Framework 5.3.18 API)

Interface representing the environment in which the current application is running. Models two key aspects of the application environment: profiles and properties. Methods related to property access are exposed via the PropertyResolver superinterface. A pr

docs.spring.io

https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#appspec-hooks-server

 

AppSpec 'hooks' 섹션 - AWS CodeDeploy

배포의 Start, DownloadBundle, Install, BlockTraffic, AllowTraffic 및 End 이벤트는 스크립팅할 수 없기 때문에 이 다이어그램에서 회색으로 표시됩니다. 그러나 AppSpec 파일의 'files' 섹션을 편집하여 Install 이벤

docs.aws.amazon.com

 

반응형

댓글