Jenkins + CodeDeploy를 이용한 Spring boot 무중단 배포

slee2·2022년 6월 30일
0

main

목록 보기
8/12

환경

  • Spring boot
    • java 11
    • spring boot 2.7.1
    • spring web
    • lombok
    • actuator
  • AWS
    • EC2
    • S3
    • CodeDeploy
  • Jenkins
  • Github

위 환경은 AWS CodeDeploy + Jenkins를 이용한 Spring boot 자동화 + Jenkins CodeDeploy plugin 오류 해결 에서 설정가능하고 이 작업 이후로 이어진다.

Nginx

# aws ec2 linux nginx 설치(nginx1) 
sudo amazon-linux-extras install nginx1 -y

# nginx 설치 확인
nginx -v
nginx version: nginx/1.20.0

# nginx 시작
sudo service nginx start
Redirecting to /bin/systemctl start nginx.service

# 리버스 프록시 설정
# 기본 설정은 유지
sudo vim /etc/nginx/nginx.conf
server {
    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-Forworded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
    }
}

# 리버스 프록시 설정
# 배포할 때 마다 포트 변경
# real1 -> 8081, real2 -> 8082
# 첫 번째 배포 시에 정상적으로 포트가 설정되는지 확인하기 위해 8082로 설정
# /etc/nginx/conf.d/service-url.inc 파일을 아래 내용으로 생성
set $service_url http://127.0.0.1:8082;

# nginx 재시작
# service-url.inc 파일을 미리 만들어두지 않을 경우 에러가 발생함
sudo service nginx restart
Redirecting to /bin/systemctl restart nginx.service

혹시 sudo service nginx restart를 했을때, 포트번호가 이미 사용중이라는 오류가 발생한다면

sudo fuser -k 80/tcp
sudo fuser -k 443/tcp

를 실행한 후에 재시작 하면 된다.

Spring boot actuator

actuator는 어플리케이션 상태를 모니터링할 수 있게 해준다.
앤드 포인트로 접근하여 해당 어플리케이션이 어떤 상태인지 알려준다.
이중에서 /health 앤드 포인트를 이용할 계획이다.

application.yml

management:
  endpoint:
  endpoints:
    web:
      base-path: /application
---
spring:
  config:
    activate:
      on-profile: test1
server:
  port: 8081
---
spring:
  config:
    activate:
      on-profile: test2
server:
  port: 8082
  • managment
    • actuator 기능중에 /health 기능 관련 설정이다.
    • /health는 기본적으로 /actuator/health 경로를 통해 상태를 확인할 수 있다.
    • 위와 같이 설정하면 /application/health를 통해 확인 가능하다.
  • profile
    • 스프링 부트에 각각 다른 profile 이름을 부여함으로써 두개의 포트를 관리한다.
    • 현재 test1, test2이름으로 만들어졌습니다.

이 요청은 test1 또는 test2 profile이 적용됐을 경우, 적용된 profile을 알려주는 요청이다.

DockerFile

FROM openjdk:11-jdk

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} testboard.jar

ARG IDLE_PROFILE
ENV ENV_IDLE_PROFILE=$IDLE_PROFILE

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${ENV_IDLE_PROFILE}", "/testboard.jar"]
  • FROM : 도커 이미지는 base 이미지부터 시작하여 층층이 올려가는 layer 구조이다. 이중에 FROMbase의 최상단이라고 생각하면된다.
  • ARG : docker build를 하게 될때 옵션으로 --build-arg 등으로 옵션을 넘겨 해당 옵션을 정의하기 위한 용도이다.
  • ENV : 환경변수 설정이다. ARG는 빌드되는 동안에만 사용한다면, - - ENV는 도커 컨테이너 내부에서 계속 사용한다고 생각하면 된다.
  • COPY : 복사.
  • ENTRYPOINT : 실행 명령어를 직접 입력하는 것이다.
    • java -jar : 자바 실행 기본 옵션이다. jar파일로 자바를 실행
    • -D : 어플리케이션 내부 properties를 설정하는 옵션이다.

appspec.yml

version: 0.0
os: linux
files:
  - source:  /
    destination: /home/ec2-user/testboard
    overwrite: yes

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

hooks:
  ApplicationStop:
    - location: scripts/stop.sh
      timeout: 360
      runas: ec2-user
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 360
      runas: ec2-user
  ValidateService:
    - location: scripts/health.sh
      timeout: 360
      runas: ec2-user

appspec.yml의 자세한 설정은 이전글의 Build 설정 을 참고

  • ApplicationStop
    • 새 버전을 다운받기 전에 현재 어플리케이션을 중지하는 주기이다.
    • 중지하기 위해 stop.sh를 만들어야 한다.
  • ApplicationStart
    • 새 어플리케이션을 시작하기 위한 주기이다.
    • 실행하기 위한 start.sh 스크립트를 작성해야한다.
  • ValidateService
    • 실행이 성공했는지 확인하기 위한 주기이다.
    • health.sh 에서 actuator 모니터링을 진행하면 된다.

profile.sh

#!/bin/bash

function find_profile()
{
    CURRENT_PROFILE=$(curl -s http://localhost/profile)

    if [ $CURRENT_PROFILE == test1 ]
    then
        IDLE_PROFILE=test2
    elif [ $CURRENT_PROFILE == test2 ]
    then
        IDLE_PROFILE=test1
    else
        IDLE_PROFILE=test1
    fi

    echo "${IDLE_PROFILE}"
}

function find_port()
{
    IDLE_PROFILE=$(find_profile)

    if [ $IDLE_PROFILE == test1 ]
    then
        IDLE_PORT=8081
    elif [ $IDLE_PROFILE == test2 ]
    then
        IDLE_PORT=8082
    fi

    echo "${IDLE_PORT}"
}

function find_switch_port()
{
    CONTAINER_ID=$(sudo docker ps -f "ancestor=test2" -q)

    if [ -z $CONTAINER_ID ]
    then
        echo "8081"
    else
        find_port
    fi
}
  • find_profile : /profile 접근을 통해 어떤 profile이 실행중인지 확인한 후에 값에 따라 ${IDLE_PROFILE}를 정한다.
    • test1 이면 test2
    • test2 이면 test1
    • 값이 없으면 test1
  • find_port : ${IDLE_PROFILE} 값으로 포트번호를 ${IDLE_PORT}로 지정한다.
    • test1이면 8081
    • test2이면 8082
  • find_switch_port : 실행중인 컨테이너를 통해 바꿔줄 포트 확인
    • -f : filter 이다. ancestor=test2 조건에 맞는 컨테이너를 찾는것이다.
      • ancestor : 공유받은 컨테이너 이미지이다.
    • -q : 컨테이너ID만 출력한다.
    • 실행중인 컨테이너가 없으면 8081
    • 그외에는 find_port

swith.sh

#!/bin/bash

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

function switch_nginx_proxy()
{
    IDLE_PORT=$(find_switch_port)

    echo "> 실행 포트 : $IDLE_PORT" >> /home/ec2-user/deploy.log
    echo "> /etc/nginx/conf.d/service-url.inc 변경" >> /home/ec2-user/deploy.log
    echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

    echo "> nginx 재시작" >> /home/ec2-user/deploy.log
    sudo service nginx restart
}

포트 번호 설정을 바꾸는 메서드가 들어있다.
service-url.inc$service_url값이 들어있고, 이 정의는 nginx에서 활용한다.

80 -> $service_url 로 넘겨주는 작업을 nginx가 해준다.
그러므로, $service_url만 바꾸면,
80 포트로 접근했을때, nginx가 바뀐 url로 넘겨준다.

stop.sh

#!/bin/bash

echo "> stop 시작" >> /home/ec2-user/deploy.log

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

IDLE_PROFILE=$(find_profile)

echo "> 실행할 profile : $IDLE_PROFILE " >> /home/ec2-user/deploy.log

CONTAINER_ID=$(sudo docker ps -f "ancestor=${IDLE_PROFILE}" -q)

if [ -z $CONTAINER_ID ]
then
    echo "> 기존에 실행되고 있던 컨테이너가 없습니다. " >> /home/ec2-user/deploy.log
else
    echo "> 기존에 실행되고 있던 컨테이너 : $CONTAINER_ID" >> /home/ec2-user/deploy.log
    sudo docker stop $CONTAINER_ID
    sudo docker rm $CONTAINER_ID
    echo "> 컨네이너 종료 완료, $CONTAINER_ID" >> /home/ec2-user/deploy.log
    sudo docker image rm $IDLE_PROFILE
    echo "> 기존 이미지 삭제 완료, $IDLE_PROFILE" >> /home/ec2-user/deploy.log
    sleep 10
fi
  • readlink -f : symbolic link 원본의 절대 경로를 출력한다.
    • $> readlink -f ./abc.sh
    • /Users/abc.sh
  • dirname : 경로에서 디렉토리 이름까지만 가져온다.
    • $> dirname /User/abc.sh
    • /User
  • source : 쉘 스크립터 실행. 이 스크립터를 통해 메서드를 읽어온다.
  • IDLE_PROFILE : profile.sh 내부 메서드를 통해 profile값을 얻어온다. 이때 얻어온 profile 값은 새롭게 실행할 profile이다.
  • CONTAINER_ID
    • 도커 명령어를 통해 이전에 실행된 컨테이너를 확인한다.
    • 없으면 없다는 출력으로 끝
    • 있으면, 해당 도커 서버 종료와 이미지 삭제를 완료

위 작업은 새로 만들 포트번호가 있는 컨테이너를 확인하고 삭제하는 단계이다.

쉽게말해 test1이 이전 profile이고, test2를 새로 빌드할 예정이라면, 현재 test2 컨테이너를 삭제하는 것이다.


deploy.log - stop.sh

첫 시작

두번째 시작


start.sh

echo "> start 시작" >> /home/ec2-user/deploy.log

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

IDLE_PROFILE=$(find_profile)
IDLE_PORT=$(find_port)

echo "> 도커 이미지 파일을 생성합니다." >> /home/ec2-user/deploy.log
sudo docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -f /home/ec2-user/testboard/Dockerfile -t ${IDLE_PROFILE} /home/ec2-user/testboard >> /home/ec2-user/deploy.log 2>/home/ec2-user/deploy_err.log
sleep 30

echo "> 도커 컨테이너를 실행합니다." >> /home/ec2-user/deploy.log
sudo docker run -p ${IDLE_PORT}:${IDLE_PORT} ${IDLE_PROFILE} >> /home/ec2-user/spring.log 2>/home/ec2-user/deploy_err.log &
sleep 10
  • ABSPATH, ABSDIR, source, IDLE_PROFILE, IDLE_PROT : 이전과 동일하다.
  • docker build
    • IDLE_PROFILE : DockerFile에서 환경변수로 지정된 변수이다.
    • -f : 도커 파일을 직접 사용하므로 이 옵션을 통해 도커파일 경로를 사용
    • -t : tag이다. 말 그대로 태그 이름 설정이다.
    • CodeDeploy 설정으로 /home/ec-user/testboard에 파일을 복사하므로 이곳에서 build를 진행한다.
    • 모든 내용을 deploy.log에 담는다.
    • 에러가 생겼을 경우, deploy_err.log에 담는다.

deploy.log - start.sh

첫 시작

두번째 시작


health.sh

#!/bin/bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)

source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh

IDLE_PORT=$(find_switch_port)

echo "> 새롭게 실행한 애플리케이션 health 확인 " >> /home/ec2-user/deploy.log
echo "> 실행 포트 : $IDLE_PORT" >> /home/ec2-user/deploy.log

for CNT in {1..10}
do
    echo "> health 확인용 반복문 시작... $CNT 회" >> /home/ec2-user/deploy.log
    UP=$(curl -s http://127.0.0.1:${IDLE_PORT}/application/health | grep 'UP')
    if [ -z "${UP}" ]
    then
        echo "> 아직 애플리케이션이 시작되지 않았습니다." >> /home/ec2-user/deploy.log
    else
        echo "> 애플리케이션이 정상적으로 실행되었습니다." >> /home/ec2-user/deploy.log
        switch_nginx_proxy
        break ;
    fi

    sleep 10
done

if [ $(CNT) -eq 10 ]
then
    echo "> 애플리케이션 실행에 실패했습니다." >> /home/ec2-user/deploy.log
    exit 1
fi

/health를 요청했을때 해당 서버가 동작하고 있다면 {"status" : "UP"} 로 날라온다. 이를 이용해 값 유무에 따라 어플리케이션 동작 유무를 결정하는 것이다.

정상 작동이 확인되면, nginx 환경변수 값을 바꾸고 재시작해준다.


deploy.log - health.sh

에러가 났을때

정상 작동을 할때


0개의 댓글