본문 바로가기

jenkins

Nginx를 활용한 무중단 Spring Boot CI/CD 구축

저번 포스팅에서 EC2 + Jenkins + Github를 활용하여 Spring Boot를 서버에 띄우는 CI/CD 구축해 보았습니다.

https://gong-story.tistory.com/40

 

AWS EC2 + Jenkins + Github 활용한 Spring Boot CI/CD 구축

이번 포스팅에서는 AWS EC2서버에 젠킨스와 Github를 연동하여 Spring Boot 프로젝트가 자동 배포되도록 CI/CD를 구축해 보려고 합니다. 전 회사에서는 gitlab에 push후에 젠킨스에 들어가서 배포 버튼을

gong-story.tistory.com

혼자만의 프로젝트라면, 배포시 중단이 되더라도 사용자가 없으니 상관이 없겠지만 실제 회사에서 사용되는 서비스들이 배포를 진행할때마다 서비스가 중단이 된다면, 사용자들이나 개발자들 모두가 피곤한 상황에 놓이게 됩니다.그래서 이번 포스팅에서는 Nginx를 추가하여 기존에 진행했던 CI/CD 구축 방식무중단 CI/CD 구축 방식으로 변경해보면서 공부해보려고 합니다. 무중단 CI/CD 구축은 아래 이미지와 같이 진행하려고 합니다.

 

1. Nginx 설치 및 기존 환경 수정

1 - 1. Nginx 설치

아래 명령어로 Nginx를 설치합니다.

sudo yum install nginx

그리고 Nginx가 잘 실행되었는지 아래 명령어로 확인해보겠습니다.

ps -ef | grep nginx

Nginx는 잘 실행 되었고, Nginx의 설정 파일을 열어서 Nginx가 저희 프로젝트를 바라 볼 수 있도록 리버스 프록시를 설정 하도록 하겠습니다. location 부분만 추가 하면 됩니다.

sudo vi /etc/nginx/nginx.conf

proxy_pass는 클라이언로부터 요청이 오면 localhost:8080으로 요청을 전달한다는 의미입니다.

Nginx의 설정파일이 변경 되었음으로 아래 명령어로 Nginx를 재시작 해보겠습니다.

sudo service nginx restart

그럼 브라우저로 접근하여 http://"퍼블릭 IPv4":8080을 입력해보면, Nginx가 요청을 대신 받아서 index.html화면을 응답값으로 전달해 줄 것입니다.

 

1 - 2. 기존환경 수정 - Publish over SSH을 사용하여 원격 서버로 *.jar파일 전송

기존에는 프로젝트 빌드후, Jenkins의 기본 경로인 "/var/lib/jenkins/workspace/프로젝트명/build/libs"경로에 *.jar형식으로 파일이 생성되었습니다. 그리고 *.jar파일을 가지고 nohup 방식으로 실행하여 프로젝트 배포를 진행하였습니다.

이번 포스팅에서 제가 Publish over SSH를 사용하여 원격서버로 *.jar파일을 전송하는 이유는 추후에 이 방법을 사용하여 원격서버로 파일을 보내 CI/CD구축을 공부할 것이기 때문입니다.

 

1 - 2 - 1. Publish Over SSH

Jenkins Dashboard -> jenkins 관리 -> Plugins에 들어와서 아래 플러그인을 설치합니다.

그 후에 Jenkins Dashboard -> jenkins 관리 -> System에 들어와서 Publish over SSH를 설정합니다.

ec2를 생성할때 가지고있던 .pem파일 안에 있던 key값을 기입합니다.

그 아래에 나머지 정보를 기입합니다.

Name : 본인이 알아볼 수 있는 이름을 넣어주면 됩니다.

Hostname : 해당 서버의 주소를 넣어주면 됩니다. (퍼블릭 IPv4)

Username : 계정명을 넣어주시면 됩니다.

Remote Directory : 작업을 진행할 경로를 넣어주면 됩니다.

그리고 Test Configuration을 클릭하여 Success가 나오면 성공입니다.

 

1 - 2 - 2. 빌드 후 조치 수정

Dashboard -> marcket -> Configuration에 들어와서 아래 정보를 기입합니다.

Name : 아까 만든 SSH Server이름입니다. 자동으로 선택되어 있습니다.

Source files : 원격 서버로 보낼 파일의 경로를 설정합니다. 절대경로가 아닌 프로젝트 및에 경로부터 기입합니다

ex) 젠킨스의 *.jar파일이 생기는 경로가 "/var/lib/jenkins/workspace/프로젝트명/build/libs"인데 프로젝트명까지 제외하고 build/libs/*.jar로 기입하셔야합니다.

Remove prefix : *.jar를 제외한 나머지경로를 기입합니다.

Remote directory : Source files가 저장될 원격 서버 폴더 경로이고, SSH Server로 지정한 서버의 원격지 폴더경로 이후부터 지정합니다.

Exec command : 실행시킬 명령어인데 저같은 경우는 일단 테스트라서 echo 'hello'라고 적어두었습니다.

 

1 - 2 - 3. 권한 오류

이 부분은 혹시 다른분들도 이런 오류가 발생하면 참고차 기록하게 되었습니다.

Publish over SSH를 사용하여 원격서버로 파일을 보내는데 아무리해도 파일 전송이 되지 않았습니다.

빌드는 되었지만, 원격서버로 파일을 보낼때에는 젠킨스 계정으로 파일을 보내기 때문에 해당 디렉토리에 jenkins 계정의 권한을 맞춰주어야 합니다. 해당 이슈가 발생하신다면 아래 블로그를 참조하시면 될 것 같습니다.

https://velog.io/@mungmnb777/%EC%A0%A0%ED%82%A8%EC%8A%A4-Permission-denied-%EC%97%90%EB%9F%AC

 

[Jenkins] 젠킨스 Permission denied 에러

Jenkins + Gitlab Webhook을 이용한 CI/CD 구축 중 만난 문제와 원인, 해결법을 알아볼게요!

velog.io

권한 문제가 해결되면서, 파일이 원격서버로 전달되었고 아래와 같이 성공 로그와 *.jar파일을 볼 수 있습니다.

 

2. Profile 설정

보통 Profile을 설정하는 이유는 하나의 프로젝트가 개발환경, 운영환경 등 다양한 환경에서 다르게 적용하기위해 작성을 합니다. 하지만, 현재 프로파일을 설정하는 이유는 하나의 EC2에서 무중단으로 배포하기위해 2개의 포트를 사용할 것이기 때문입니다.

먼저, 실행중인 Profile을 찾기위한 API를 만들어보겠습니다.

@RestController
public class profileController {

    @Autowired
    private Environment env;

    @GetMapping("/profile")
    public String getProfile() {
        return Arrays.stream(env.getActiveProfiles())
                .findFirst()
                .orElse("");
    }
}

API를 만들었으면, application.yml파일에 Profile 및 Port설정을 해보겠습니다.

"---"라는 구분자로 환경을 구분하여 줍니다.

기본 로컬에서 사용하는 환경 default
~~
~~

---
spring:
  config:
    activate:
      on-profile: was1
server:
  port: 8090

---
spring:
  config:
    activate:
      on-profile: was2
server:
  port: 8091

 

3. 배포 스크립트 작성

3 - 1. deploy.sh 스크립트 작성

먼저, 스크립트를 저장할 경로와 폴더를 ec2에 미리 생성해보도록 하겠습니다.

// *.jar파일을 모아두는 경로
/home/ec2-user/app/nonstop/jar

// 젠킨스에서 build시 *.jar파일이 생성되는 경로
/home/ec2-user/app/nonstop/marcket/build/libs

이제 배포하는 스크립트를 생성하도록 하겠습니다.

vim ~/app/nonstop/deploy.sh
#!/bin/bash
BASE_PATH=/home/ec2-user/app/nonstop
BUILD_PATH=$(ls $BASE_PATH/marcket/build/libs/*.jar)
JAR_NAME=$(basename $BUILD_PATH)
echo "> build 파일명: $JAR_NAME"

echo "> build 파일 복사"
DEPLOY_PATH=$BASE_PATH/jar/
cp $BUILD_PATH $DEPLOY_PATH

echo "> 현재 구동중인 Set 확인"
CURRENT_PROFILE=$(curl -s http://localhost/profile)
echo "> $CURRENT_PROFILE"

# 쉬고 있는 set 찾기: was1이 사용중이면 was2가 쉬고 있고, 반대면 was1이 쉬고 있음
if [ $CURRENT_PROFILE == was1 ]
then
  IDLE_PROFILE=was2
  IDLE_PORT=8091
elif [ $CURRENT_PROFILE == was2 ]
then
  IDLE_PROFILE=was1
  IDLE_PORT=8090
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> was1을 할당합니다. IDLE_PROFILE: was1"
  IDLE_PROFILE=was1
  IDLE_PORT=8090
fi

echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-marcket.jar
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION

ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH

echo "> $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(pgrep -f $IDLE_APPLICATION)

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

echo "> $IDLE_PROFILE 배포"
echo "> $IDLE_PROFILE "
nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH > /dev/null 2>&1 &

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

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$IDLE_PORT/actuator/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

현재 프로젝트가 1개 올라와 있는상태입니다.

이 상태에서 방금 생성한 deploy.sh을 실행해보도록 하겠습니다.

 sudo sh deploy.sh

이미지를 보시면 profile이 was1인 프로젝트가 구동중인것을 볼 수 있습니다.

여기서 curl -s http://localhost:8090/actuator/health라는 명령어가 있는데 이는 아래 라이브러리를 다운받아야합니다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'

3 - 2. Nginx 동적 프록시 설정

배포가 완료되면 어플리케이션 실행 된후, Nginx가 기존에 바라보던 Profile의 반대편을 바라보도록 변경하는 과정이 필요합니다. 

sudo vim /etc/nginx/nginx.conf

include /etc/nginx/conf.d/service-url.inc 안에 있는 service_url이라는 변수를 proxy_pass로 받는다는 의미입니다.

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

그리고 아래 코드와 같이 저장합니다.

set $service_url http://127.0.0.1:8090;

저장을 하였으면 nginx를 restart 시켜줍니다.

sudo service nginx restart

테스트를 위해  curl -s http://localhost/profile명령어를 실행해보면 아래와 같이 was1이 노출되는 것을 볼 수 있습니다.

3 - 3. Nginx 스크립트 작성

이제는 배포시점에 Nginx에서 바라보는 Port를 자동으로 변경해주는 스위치 스크립트를 작성해보려고 합니다.

sudo vi ~/app/nonstop/switch.sh

스크립트 내용은 아래와 같습니다.

#!/bin/bash
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/profile)

# 쉬고 있는 set 찾기: was1이 사용중이면 was2가 쉬고 있고, 반대면 was1이 쉬고 있음
if [ $CURRENT_PROFILE == was1 ]
then
  IDLE_PORT=8091
elif [ $CURRENT_PROFILE == was2 ]
then
  IDLE_PORT=8090
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> 8090을 할당합니다."
  IDLE_PORT=8090
fi

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

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

echo "> Nginx Reload"
sudo service nginx reload

이 스크립트에서 제일 중요한 부분은 "sudo tee /etc/nginx/conf.d/service-url.inc"부분입니다.

echo명령어를 통한 출력과 저장을 동시에 한다고 생각하시면 됩니다.

자세한 설명은 tee명령어를 검색해보시면 될 것 같습니다.

 

저장 후에, swtich.sh파일에 실행 권한을 줍니다.

chmod +x ~/app/nonstop/switch.sh

이제 switch.sh을 테스트하기 위해 deploy.sh을 한번 더 실행시켜 2개의 *.jar파일을 실행시키도록 하겠습니다.

was1, was2의 profile을 가진 프로젝트가 2개가 떠있는 것을 볼 수 있고 현재 Nginx는 was1을 바라보고 있습니다.

이제 switch.sh파일을 실행시켜 Nginx가 바라보는 포트가 변경되는지를 확인해보겠습니다.

sudo sh switch.sh

실행후에 service_url포트가 변경된 것을 확인할 수 있습니다.

그렇다면 Nginx의 Proxy_pass도 변경되었겠죠??

 

이제 마지막으로 deploy.sh에 switch.sh을 추가하여 deploy.sh 후에 자동으로 switch.sh 스크립트가 실행되도록 해보겠습니다.

// deploy.sh 마지막 부분에 추가
echo "> 스위칭"
sleep 10
/home/ec2-user/app/nonstop/switch.sh

이제 deploy.sh을 실행 시키면 switch.sh이 실행되는지 확인해보도록 하겠습니다.

배포 스크립트 후에 스위치 스크립트가 실행되는 것을 확인하였습니다.

 

4. 마무리

이번 포스팅에서는 Nginx를 추가하여 기존에 진행했던 CI / CD 구축 방식을 무중단 CI / CD 구축 방식으로 변경해보았습니다. Nginx를 직접 적용해보는 과정에서 Nginx의 여러 기능 및 사용방법을 알아볼 수 있었고, 쉘 스크립트에대한 공부도 많이 되었던 것 같습니다.

'jenkins' 카테고리의 다른 글

AWS EC2 + Jenkins + Github 활용한 Spring Boot CI/CD 구축  (1) 2023.08.30