항해 75일차
Travis CI와 CodeDeploy로 CI/CD 구성을 하였다.
배포 자동화까지 되었으니 무중단 배포까지 진행했다.
긴 시간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기 때문에 서비스가 종료된다.
서비스를 정지하지 않고 배포하는 것을 무중단 배포라고 한다.
Nginx가 가지고 있는 여러 기능 중 리버스 프록시가 있다.
리버스 프록시란 Nginx가 외부의 요청을 받아 백엔드 서버로 요청을 전달하는 행위를 이야기 한다.
리버스 프록시 서버(엔진엑스)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션들이 처리한다.
우리는 이 리버스 프록시를 통해 무중단 배포 환경을 구축해 볼 예정이며
Nginx를 이용한 무중단 배포를 하는 이유는 가장 저렴하고 쉽다.
하나의 EC2 혹은 리눅스 서버에 엔진엑스 1대와 스프링 부트 Jar를 2대를 사용한다.
Nginx는 80(http), 443(https) 포트를 할당합니다.
스프링 부트1은 8081 포트로 실행
스프링 부트2는 8082 포트로 실행
사용자는 서비스 주소로 접속(80 혹은 443 포트).
Nginx는 사용자의 요청을 받아 현재 연결된 스프링 부트로 요청을 전달
스프링 부트1 즉, 8081 포트로 요청을 전달한다고 할때
스프링 부트2는 Nginx와 연결된 상태가 아니니 요청받지 못한다.
최신 버전으로 신규 배포가 필요하면, Nginx와 연결되지 않은 스프링 부트2(8082 포트)로 배포한다.
먼저 EC2에 Nginx를 설치 및 실행 한다.
sudo apt-get update
sudo apt-get install nginx
sudo service nginx start
Nginx가 현재 실행 중인 스프링 부트 프로젝트를 바라볼 수 있도록 프록시 설정을 먼저 해준다.
sudo vi /etc/nginx/sites-enabled/default
조금 내려가서 아래 이미지와 동일하게 설정해준다.
include /etc/nginx/conf.d/service-url.inc;
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;
sudo vim /etc/nginx/conf.d/service-url.inc
set $service_url http://127.0.0.1:8081;
저장하고 종료한(:wq) 뒤 Nginx 재시작
sudo service nginx restart
@RequiredArgsConstructor
@RestController
public class ProfileController {
private final Environment env;
/* NginX가 어느 포트의 서버를 바라보고있는지 확인하기 위한 컨트롤러 */
@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);
}
}
real1, real2, profile 생성
application-real1.properties
server.port=8081
server.port=8082
profile API 추가를 완료했다면 배포 스크립트를 작성해보자.
먼저 기존 배포하였던 test와 중복되지 않기 위해 EC2에 test2 디렉토리를 생성한다.
mkdir ~/app/test2 && mkdir ~/app/test2/zip
무중단 배포는 앞으로 test2를 사용하고 appspec.yml 역시 test2로 배포되도록 수정한다.
version: 0.0
os : linux
files :
- source : /
destination: /home/ubuntu/app/test2/zip
overwrite : yes
무중단 배포를 진행할 스크립트들은 총 5개다.
- stop.sh
- 기존 Nginx에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료
- start.sh
- 배포할 신규 버전 스프링 부트 프로젝트를 stop.sh로 종료한 'profile'로 실행
- health.sh
- 'start.sh'로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크
- switch.sh
- 엔진엑스가 바라보는 스프링 부트를 최신 버전으로 변경
- profile.sh
- 앞선 4개 스크립트 파일에서 공용으로 사용할 'profile'과 포트 체크 로직
hooks:
AfterInstall:
- location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료
timeout: 60
runas: ubuntu
ApplicationStart:
- location: start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작
timeout: 60
runas: ubuntu
ValidateService:
- location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인
timeout: 60
runas: ubuntu
#!/usr/bin/env bash
# bash는 return value가 안되니 *제일 마지막줄에 echo로 해서 결과 출력*후, 클라이언트에서 값을 사용한다
# 쉬고 있는 profile 찾기: real1이 사용중이면 real2가 쉬고 있고, 반대면 real1이 쉬고 있음
function find_idle_profile()
{
RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함)
then
CURRENT_PROFILE=real2
else
CURRENT_PROFILE=$(curl -s http://localhost/profile)
fi
if [ ${CURRENT_PROFILE} == real1 ]
then
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
}
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
IDLE_PORT=$(find_idle_port)
echo "> $IDLE_PORT 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})
echo "> 현재 IDLE_PID : ${IDLE_PID}"
if [ -z ${IDLE_PID} ]
then
echo "> 구동중인 PID : ${IDLE_PID}"
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $IDLE_PID"
kill -15 ${IDLE_PID}
sleep 5
fi
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
REPOSITORY=/home/ubuntu/app/step3
PROJECT_NAME=hanghae8-admin
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.properties,classpath:/application-$IDLE_PROFILE.properties \
-Dspring.profiles.active=$IDLE_PROFILE \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
#!/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
#!/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
포트 전환하면서, 바꾼거말고 다른거 kill
if [ ${IDLE_PORT} == 8081 ]
then
KILL_PORT=8082
IDLE_PID=$(lsof -ti tcp:${KILL_PORT})
echo "> ${KILL_PORT} 포트를 종료합니다."
kill -15 ${IDLE_PID}
else
KILL_PORT=8081
IDLE_PID=$(lsof -ti tcp:${KILL_PORT})
echo "> ${KILL_PORT} 포트를 종료합니다."
kill -15 ${IDLE_PID}
fi
echo "> 엔진엑스 Reload"
sudo service nginx reload
}