제가 최근 진행한 식구하자 프로젝트에서 무중단 배포를 구축하였는데, 이번 포스팅에서 그 과정을 다루도록 하겠습니다
무중단 배포를 진행하기전에 AWS ec2를 통한 배포와 CI/CD환경은 구축이 되있다 가정하고 진행하겠습니다.
AWS ec2를 통한 배포와 Github Actions을 활용한 CI/CD환경 파이프라인 구축이 궁금하신 분들은
👉이전 시간에 진행한 포스팅 참고!
보통 CI/CD 환경이 구축이 되면 , Master 브랜치에 Push만 되면 자동으로 빌드 & 테스트 & 배포가 자동으로 이루어집니다.
하지만! 배포하는 시간 동안은 어플리케이션이 종료가 됩니다.
긴 시간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜놓기 때문에 서비스가 안됩니다. 하지만 최근 웹 서비스들은 대부분 배포하기 위해 서비스를 정지시키는 경우가 없습니다.
어떻게 서비스의 정지 없이 배포를 계속 할 수 있는지 이번 시간에 확인하고 서비스에 적용해보겠습니다.
말 그래로서비스가 중단되지 않는 상태로 새로운 버전을 사용자에게 계속해서 배포하는 것입니다. 기존 CI/CD 환경을 통한 배포는 개발자 중심이라면, 무중단 배포는 사용자 중심이라고 볼수있습니다.
다양한 방법이 있지만, Nginx를 이용한 무중단 배포를 하는 이유는 간단합니다. 가장 저렴하기 때문입니다.
기존에 쓰던 EC2에 그대로 적용하면 되기 때문에 배포를 위해 AWS EC2 인스턴스가 하나더 필요하지 않습니다.
추가로 이 방식은 꼭 AWS와 같은 클라우드 인프라가 구축되있지 않아도 쓸수 있는 범용적인 방법입니다.
바로 설명드리는 구조와 무중단 배포 과정에 대해서 꼭 이해하고 진행하시길 바랍니다!! 저도 진행할때 과정을 이해하고 진행해서 좀 더 쉽게 구현할 수 있었습니다!!
구조는 간단합니다.
하나의 EC2 혹은 리눅스 서버에 Nginx 1대와 스프링부트 jar를 2대를 사용하는 것입니다.
Nginx는 80(http), 443(https) 포트를 할당하고,
스프링부트1은 8081포트로,(포트 번호는 상관없습니다!)
스프링부트2는 8082포트로 실행합니다.
그럼 아래와 같은 구조가 됩니다.
운영 과정은 다음과 같습니다.
📌Tip)
이렇게 Nginx가 외부의 요청을 받아 뒷단 서버로 요청을 전달하는 행위를 리버스 프록시라고 합니다.
이런 리버스 프록시 서버(Nginx)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹서버들이 처리합니다.
대신 외부 요청을 뒷단 서버들에게 골고루 분배한다거나, 한번 요청왔던 js, image, css등은 캐시하여 리버스 프록시 서버에서 바로 응답을 주거나 등의 여러 장점들이 있습니다.
이제 과정에 대해 이해를 했으니, 바로 진행해보겠습니다!
이 포스팅은 EC2 인스턴스, CI/CD 파이프라인 구축 및 S3&IAM 설정이 되있는걸 가정하고 진행합니다‼️
먼저 Nginx를 설치하겠습니다.
EC2에 접속해서 아래 명령어로 Nginx를 설치합니다.
sudo yum install nginx
설치가 완료되셨으면 아래 명령어로 Nginx를 실행합니다.
sudo systemctl start nginx
Nginx 잘 실행되었는지 아래 명령어로 확인해봅니다.
sudo systemctl statys nginx
Nginx가 잘 실행되었습니다!
설치가 잘되었으면 nginx 설정 파일을 열어서
sudo vi /etc/nginx/nginx.conf
설정 내용 중 server 아래의 location / 부분을 찾아서 아래와 같이 추가합니다.
proxy_pass http://localhost:8080;
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;
수정이 끝나셨으면 :wq로 저장 & 종료 하시고, Nginx를 재시작하겠습니다.
sudo service nginx restart
다시 브라우저로 접속해서 Nginx 시작페이지가 보이던 화면을 새로고침해보시면!
Nginx가 스프링부트 프로젝트를 프록시 하는것이 확인됩니다!
본격적으로 무중단 배포 작업을 진행해보겠습니다.(저는 탄력적 IP를 사용하였습니다!)
API를 만들기전에 이후에 health check를 위해 actuator 의존성을 build.gradle에 추가 해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-actuator'
그 이후 실행중인 프로젝트의 Profile이 뭔지 확인할 수 있는 API를 만들겠습니다.
WebRestController.java에 아래와 같이 API 메소드를 하나 추가합니다!
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class WebRestController {
private final Environment env;
@GetMapping("/profile")
public String getProfile () {
return Arrays.stream(env.getActiveProfiles())
.findFirst()
.orElse("");
}
}
profile값을 반환하는 API가 완성되었으니 운영 환경의 yml파일을 추가해보겠습니다.
운영 환경의 yml은 프로젝트 내부가 아닌 외부에 생성하겠습니다.
본인이 원하는 디렉토리에 real-application.yml을 생성합니다.
저는 /resources/real-application.yml위치에 생성했습니다.
그리고 real-application.yml에는 아래 코드를 등록합니다.
---
spring:
profiles: set1
server:
port: 8081
---
spring:
profiles: set2
server:
port: 8082
즉, set1/set2 profile을 8081, 8082 포트를 갖도록 설정한 것입니다
외부에 있는 이 파일을 프로젝트가 호출할 수 있도록 Application.java 코드를 아래와 같이 변경합니다.
@SpringBootApplication
@EnableJpaAuditing
public class PlantProjectApplication {
public static final String APPLICATION_LOCATIONS = "spring.config.location="
+ "classpath:application.properties,"
+ "classpath:real-application.yml";
public static void main(String[] args) {
new SpringApplicationBuilder(PlantProjectApplication.class)
.properties(APPLICATION_LOCATIONS)
.run(args);;
}
만약 yml파일이 /app/config 에 있으시다하면, classpath대신 /app/config/real-application.yml로 바꾸시면 됩니다!
스프링부트 프로젝트가 실행될때, 프로젝트 내부에 있는 application.yml과 real-application.yml를 모두 불러오도록 하였습니다.
자 그럼 한번 잘 불러오는지 확인해보겠습니다.
IntelliJ에서 cmd+shift+a(맥 기준!)를 사용해 Edit Configuration을 검색합니다.
프로젝트 Application을 선택후 좌측 상단의 Copy 버튼을 클릭해서 설정 내용을 복사합니다.
복사된 설정 내용을 아래와 같이 set1을 Profile로 지정한 실행환경으로 수정합니다.
새로 생성된 실행환경을 선택하고 실행해보시면!
브라우저에서 localhost:8081/api/profile 로 접속해보시면!
set1이 반환되는것이 확인 됩니다!
set2도 set1 설정 방식과 동일하게 진행하시면 됩니다!
yml파일을 .gitignore 시키셨다면 따로 EC2 인스턴스에도 똑같이 real-applicaiton.yml 설정파일을 추가하시면 됩니다!!
자 그럼 이제 본격적인 배포 스크립트를 한번 생성해보겠습니다.
작성하기전에 스프링 부트 루트/bulid/libs에 jar파일이 존재하는지 확인해야됩니다!
확인 후
jar 파일 들을 모아놓을 디렉토리를 생성합니다!
저는 jar라고 만들었습니다
mkdir ~/etc/sercer/PlantBackend/jar
여기서 파일 경로들은 본인 ec2에 있는 스프링 부트 폴더 경로 기준으로 하시면 됩니다!
스크립트 파일을 생성합니다.
sudo vim ~/etc/sercer/PlantBackend/scripts/deploy.sh
스크립트 내용은 아래와 같습니다.
#!/bin/bash
BASE_PATH=/etc/server/PlantBackend
BUILD_PATH=$(ls $BASE_PATH/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/api/profile)
echo "> $CURRENT_PROFILE"
# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
if [ $CURRENT_PROFILE == set1 ]
then
IDLE_PROFILE=set2
IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
IDLE_PROFILE=set1
IDLE_PORT=8081
else
echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
echo "> set1을 할당합니다. IDLE_PROFILE: set1"
IDLE_PROFILE=set1
IDLE_PORT=8081
fi
echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-PlantBackend.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"
kill -15 $IDLE_PID
sleep 5
fi
echo "> $IDLE_PROFILE 배포"
nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH &
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
echo "> 스위칭"
sleep 10
/etc/server/PlantBackend/scripts/switch.sh
내용이 길기 떄문에 아래 스크립트 설명을 읽으시면서 진행하시면 됩니다👍
+)
CURRENT_PROFILE=$(curl -s http://localhost/api/profile)
현재 Profile을 curl을 통해 확인합니다.
curl에서 -s 은 silent란 뜻으로 상태진행바를 노출시키지 않는 옵션입니다.
만약 이 옵션을 주지 않은채 사용하시면 curl 실행시 노출됩니다.
(예시)
response=IDLE_PORT/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
스크립트가 다 작성되셨으면 저장(:wq)합니다.자 그럼 실제로 이 스크립트를 한번 실행해볼까요?
아래 명령어로 스크립트를 실행합니다.
sh ~/etc/server/PlantBackend/scripts/deploy.sh
결과를 보시면!!
성공적으로 set1을 profile을 가진 스프링부트 프로젝트가 실행되었습니다!
하지만 여기서 끝이 아니겠죠?
Nginx가 set1과 set2를 번갈아가면서 바라볼 수 있는(프록시) 환경이 필요합니다!
배포가 완료되면 어플리케이션 실행 된후, Nginx가 기존에 바라보던 Profile의 반대편을 바라보도록 변경하는 과정이 필요합니다.
먼저 Nginx의 설정쪽으로 한번 가볼까요?
sudo vim /etc/nginx/nginx.conf
그리고 location / 부분을 찾아 아래와 같이 변경합니다.
include /etc/nginx/conf.d/service-url.inc;
location / {
proxy_pass $service_url;
이 코드가 하는 일은 다음과 같습니다
그럼 service-url.inc 파일을 생성해보겠습니다.
sudo vim /etc/nginx/conf.d/service-url.inc
그리고 아래 코드를 입력합니다.
set $service_url http://127.0.0.1:8081;
저장하셨으면 변경 내용 반영을 위해 nginx restart를 실행합니다.
sudo systemctl restart nginx
이제는 이렇게 동적 프록시 환경이 구축된 Nginx을 배포 시점에 바라보는 Profile을 자동으로 변경하도록 스위치 스크립트를 생성하겠습니다.
sudo vim ~/etc/server/PlantBackend/scripts/switch.sh
스크립트 내용은 아래와 같습니다.
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/api/profile)
# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
if [ $CURRENT_PROFILE == set1 ]
then
IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
IDLE_PORT=8081
else
echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
echo "> 8081을 할당합니다."
IDLE_PORT=8081
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/api/profile)
echo "> Nginx Current Proxy Port: $PROXY_PORT"
echo "> Nginx Reload"
sudo service nginx reload
저장하신뒤 switch.sh에 실행권한을 줍니다.
sudo chmod +x ~/etc/server/PlantBackend/scripts/switch.sh
그럼 이제 스위치 스크립트를 실행해볼까요?
먼저 현재는 set1만 실행된 상태인데 set2도 실행시키겠습니다.
sh ~/etc/server/PlantBackend/scripts/switch.sh
switch.sh도 기능이 정상적으로 작동되는게 확인됩니다!
deploy.sh에서 제일 밑에 아래 코드덕분에 switch.sh는 따로 실행 시키지 않아도 자동으로 실행됩니다!
echo "> 스위칭"
sleep 10
~/app/nonstop/switch.sh
실제 배포에 적용하기 위해 프로젝트 폴더 /scripts아래에 execute-deploy.sh 코드를 아래와 같이 변경합니다.
#!/bin/bash
/home/ec2-user/app/nonstop/deploy.sh > /dev/null 2> /dev/null < /dev/null &
그리고 마지막으로 appspec.yml도 아래와 같이 변경합니다.
version: 0.0
os: linux
files:
- source: /
destination: /etc/server/PlantBackend # source에서 지정된 파일을 받을 위치, 이후 jar를 실행하는 등은 destination에서 옮긴 파일들로 진행
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
AfterInstall:
- location: scripts/execute-deploy.sh
timeout: 180
자! 모두다 수정하셨으면 git commit & push를 실행합니다.
브라우저를 열어 웹 사이트 주소로 접속해보면 정지없이 변경사항이 적용되는걸 확인 하실 수 있습니다!
덕분에 도움 많이 됬습니다! 감사합니다 :)