Nginx를 이용하여 무중단 배포하기
들어가기에 앞서
Nginx는 아파치와 더불어 가장 많이 사용되는 웹 서버이다. Nginx가 가진 정말 많은 기능들이 있지만 이번엔 '리버스 프록시' 라는 기술을 활요하여 서버에서 무중단 배포를 하는 방법을 알아보자.
리버스 프록시란?
프록시 서버의 종류는 두 종류가 있다.
- 포워드 프록시 : 클라이언트와 웹 서버 사이에서 중계역할을 해준다.
- 리버스 프록시 : 클라이언트와 내부 서버를 연결하는 프록시이다.
여기서 리버스 프록시는 클라이언트가 요청한 내용을 어떤 서버로 보내야할지 결정하는 역할을 해줄 수 있다. 만약 2개의 WAS를 실행시키고있다고 했을 때 클라이언트가 요청을 했을 때 어떤 WAS로 요청을 전송할지 선택할 수 있는 것이다. 이런 역할을 로드벨런서라고 한다.
로드벨런서의 역할은 부하를 줄이는 대도 사용할 수 있지만 이 포스트의 주제처럼 1개의 서비스를 2개의 WAS로 실행시켜서 무중단배포를 위해 사용할 수도 있다.
Nginx설정
필자는 AWS의 EC2(Linux)를 사용하였다.
Nginx가 정상적으로 설치되었다면 /etc/nginx/nginx.conf 에서 nginx에 대한 설정이 가능하다.
해당 파일에 들어가면 http{ } 안에 server{ } 안에 include /etc/nginx/default.d/*.conf가 있다. 이 아래에 새로운 파일을 include할 것이다.
include /etc/nginx/conf.d/service-url.inc;
를 입력해준다. 이렇게 해주면 service-url.inc내부에 있는 변수를 사용할 수 있게 된다.
그 이후 바로 아래에 location을 만들어줄 것이다.
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;
}
- proxy_pass $service_url : $service_url는 include한 파일에 들어있는 변수이다. Nginx로 들어오는 요청을 해당 아이피와 포트로 넘겨준다.
- proxy_set_header X-Real-IP $remote_addr : 요청 클라이언트의 실제 IP주소를 헤더에 저장한다.
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for : 요청 클라이언트의 IP를 헤더에 저장한다. X-Real-IP와는 다르게 여기에 이 값은 프록시같은 서버를 통해 들어오는 IP를 받는 것이다.
- proxy_set_header Host $http_host : 요청 클라이언트의 Host주소를 헤더에 저장한다.
Nginx를 리버스 프록시로 사용하기 때문에 이 요청도 WAS로 전달되면 Nginx의 주소가 전달된다. 그렇기 때문에 클라이언트의 요청 주소를 정확히 알아서 WAS로 함께 전달해주어야 한다.
다음은 service-url.inc이다.
set $service_url http://127.0.0.1:8080;
Nginx가 이 주소로 요청을 할 것이다. 2개의 WAS를 각각 다른 포트번호로 동작시킬 것이다.(8081, 8082) 지금은 8080으로 되있지만 뒤에서 살펴볼 스크립트를 이용해서 그때그때 변경할 포트번호로 덮어쓰면서 내용을 바꿔줄 것이다.
2개의 포트로 나누기
2개의 WAS를 동작시켜야하기 때문에 8081과 8082 2개의 포트를 사용할 것이다. 먼저 설정파일(application.yml)을 분리해주어야 한다.
## application-rea11
spring:
profiles:
group:
real1:
real-db
server:
port: 8081
## application-rea12
spring:
profiles:
group:
real2:
real-db
server:
port: 8082
설정파일을 분리하는 자세한 내용은 [Spring/Boot] - 스프링 설정 환경 분리 를 참조해주길 바란다.
이제 /profile API를 만들어서 현재 사용중인 포트가 real1인지 real2인지 찾아줄것이다.
@RequiredArgsConstructor
@RestController
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);
}
}
지금 사용하는 것이 real1인지 real2인지 찾아서 반환해주는 API이다.
스크립트 작성
무중단배포를 하는 과정은 다음과 같이 진행될 것이다.
- 새로운 내용을 push
- CI툴을 이용해서 자동 빌드
- CD툴을 이용해서 자동 배포
여기까지는 Nginx외의 역할이다. Nginx의 역할은 다음과 같이 될 것이다.
- 2개의 서버중 현재 Nginx가 바라보고 있지 않은 WAS를 찾는다.
- 해당 WAS를 중지 시키고 업데이트된 프로젝트로 새로 실행시킨다.
- Nginx가 바라보는 WAS를 새로 실행된 WAS를 바로보도록 수정한다.
이러한 동작을 자동으로 실행할 수 있는 스크립트를 만들어 줄 것이다. 총 5개의 스크립트를 만들것이다.
- profile.sh : 사용하지 않는 포트를 찾아 줄 스크립트. 나머지 4개의 스크립트에서 공용으로 사용할 예정이다.
- stop.sh : Nginx가 바라보고있지 않지만 실행중인 WAS 종료
- start.sh : 업데이트된 프로젝트 실행
- health.sh : 새롭게 실행시킨 프로젝트가 잘 실행됐는지 체크
- switch.sh : health.sh에서 프로젝트가 잘 실행된 것을 확인되면 기존의 WAS에서 새로운 WAS를 바라보도록 교체
먼저 프로젝트의 가장 상위위치에 script폴더를 생성해준다. 위치는 build.gradle과 같은 위치이다.
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)
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
}
두 개의 함수를 만들었다.
- find_idle_profile() : 현재 사용하지 않는 설정이름을 찾아준다.(real1 or real2)
- find_idle_prot() : 현재 사용하지 않는 포트를 찾아준다.(8081 or 8082)
stop.sh를 만들자.
#!/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})
if [ -z ${IDLE_PID} ]
then
echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $IDLE_PID"
kill -15 ${IDLE_PID}
sleep 5
fi
현재 Nginx와 연결되지 않은 WAS의 포트를 찾아서 실행중이면 종료시키고 실행중이 아니라면 그대로 끝낸다.
start.sh를 만들자.
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
REPOSITORY=/home/ec2-user/app/auto
PROJECT_NAME=travelDiary ## 프로젝트 이름
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.yml,/home/ec2-user/app/application-real-db.yml \
-Dspring.profiles.active=$IDLE_PROFILE \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
build된 jar파일을 실행시키는 작업이다.
마지막 부분에 java -jar 부분이 jar파일을 실행시키는 코드이다. 이 부분은 필자는 처음에 조금 헷갈렸었는데 -Dspring.config.location=.... 부분은 적용시킬 설정파일들을 지정하는 부분이다.
classpath:/ 으로 시작하는 것들은 jar안에 포함되있는 파일을 가져오는 것이고 /home/ec2-user/app/ 으로 시작하는 것은 .gitignore에 등록되서 깃에 올라가지 않은 파일들이다. db같은 경우는 연결주소와 id, pw가 포함되있기 때문에 ec2에 직접 만들어서 저장하고 불러오도록 설정하는 것이다.
- java -jar앞에 nohup을 붙이면 로그를 nohup파일 내부에 적게된다.
- 2>&1을 하게되면 표준에러를 표준출력과 동일한 곳에 보여준다는 것이다.
- 마지막에 붙은 & 은 백그라운드에서 실행한다는 의미이다.
-Dspring.profiles.active=$IDLE_PROFILE 은 application-xxxx 에서 xxxx부분을 지정해서 해당하는 파일을 실행한다는 의미이다. real1과 real2중에 선택되어 실행될 것이다. 즉 8081 또는 8082로 실행될 것이다.
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 'prod' | wc -l)
if [ ${UP_COUNT} -ge 1 ]
then # $up_count >= 1 ("prod" 문자열이 있는지 검증)
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
현재 Nginx에 연결되지 않은 포트번호를 가져온 뒤 해당 포트번호 실행중인지 체크한다. health.sh 이전에 start.sh가 실행되기 때문에 start.sh가 정상적으로 동작했다면 현재 실행중일것이기 때문에 실행중인것이 확인되면 switch.sh를 호출해서 Nginx가 바라보는 방향을 바꿔준다.
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
}
Nginx에 연결되지 않은 포트번호를 가져온 뒤 앞서 만들었던 service-url.inc에 덮어씌운다. 그러면 이제 Nginx를 설정한 대로 Nginx가 해당포트를 바라보게 될 것이다.
마지막에 sudo service nginx reload로 Nginx를 reload해주어야 한다.
이렇게 Nginx를 이용한 무중단 배포를 마무리하였다.
물론 이 작업이 이루어지기 전에 CI/CD에 대한 설정이 완료되어 있어야 할 것이다. 그렇지 않으면 빌드파일도 직접 만들어야되고 해당 스크립트들도 직접 수동으로 실행시켜야한다.
해당 내용은 '스프링 부트와 aws로 혼자 구현하는 웹 서비스' 책의 내용을 참고하여 작성한 내용입니다.
'Nginx' 카테고리의 다른 글
Nginx 웹 서버 알아보기 (0) | 2022.06.25 |
---|