Nginx 를 이용하여 SpringBoot 무중단 배포하기
작성일
구조
- Nginx 1대, 스프링부트 jar 2대
- Nginx에는 80(http), 443(https) 포트를 할당합니다.
- 스프링부트 jar1에는 8081포트로 , 스프링부트 jar2에는 8082포트로 실행합니다.(포트는 원하시는 포트를 사용하시면 됩니다.)
- 구조는 아래의 그림같이 형성
위 그림의 동작 과정
- 사용자는 80, 443 포트로 서비스에 접속
- Nginx 는 해당 요청을 받아 현재 동작중인 스프링부트 Jar1(Port: 8081) 으로 전달함
- 스프링부트 Jar2(Port: 8082) 는 현재 동작중이지 않기 때문에 받지 못함
위 그림의 신규 버전 배포시 동작 과정
- 2.0 버전으로 신규 배포가 진행되면 현재 동작중이지 않은 스프링부트 Jar2(8082) 로 배포
- 배포하는 동안에는 사용자는 스프링부트 Jar1(8081)를 계속해서 바라보고 있는 중
- 배포가 정상적으로 끝난다면, 스프링부트 Jar2(8082)의 구동 여부를 확인
- 정상 구동 중이라면 nginx 는 스프링부트 Jar2(8082)를 바라보도록 설정
배포에 문제가 생길 시에는 정상 구동 중인 스프링부트 Jar로 돌아감
위 그림 동작 과정
- 2.1 버전으로 신규 배포가 진행되면 현재 동작중이지 않은 스프링부트 Jar1(8081)로 배포
- 배포하는 동안에는 사용자는 스프링부터 Jar2(8082)를 계속해서 바라보고 있는 중
- 배포 도중 문제가 생겼다면, nignx 는 그대로 스프링부트 Jar2(8082)를 바라보도록 함
- 배포의 문제를 확인하고 다시 배포를 진행
프로젝트 경로
# /usr/local/web
.
├── config - 무중단배포 포트관련 설정파일
├── cutelovelycat - 프로젝트 경로 (git)
│ ├── build
│ ├── gradle
│ └── src
└── nonstop - 무중단배포
├── cutelovelycat
└── jar - 배포시 실제 적용되는 jar 파일
프로젝트 settings
1) nginx 설치
-
nginx 저장소 추가
yum 저장소에는 nginx 라이브러리가 없기 때문에 저장소를 임의로 추가
vi /etc/yum.repos.d/nginx.repo
nginx.repo 파일 만들고 아래 내용 추가
[nginx] name=nginx repo baseurl=http://nginx.org/packages/centos/7/$basearch/ gpgcheck=0 enabled=1
-
nginx 설치
yum install -y nginx
-
nginx 서비스 등록 및 시작
# 서비스 등록 systemctl enable nginx # 서비스 시작 systemctl start nginx # 서비스 상태 확인 systemctl status nginx
-
nginx 설정파일 수정
/etc/nginx/conf.d/*.conf
server { listen 80; listen [::]:80; server_name localhost; 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-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; } }
service-url.inc
set $service_url http://127.0.0.1:8081;
nginx 재시작
systemctl restart nginx
참고 Nginx 동적 프록시 설정
nginx 가 set1(port: 8081) 과 set2(port: 8082) 를 번갈아가면서 바라보도록 하는 프록시 설정location / { # 동적으로 Proxy Pass 를 변경 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; }
2) github repository 연결
-
SSH key 파일 확인 및 public key 등록
cd ~/.ssh ls
위의 사진처럼 키파일이 있다면 github settings 에서 ssh key 를 등록해주면 된다.
주의! 반드시 퍼블릭키를 등록해야 한다 (*.pub)
참고
[Git (7)] Github 비밀번호 입력 없이 pull/push 하기(github ssh key 설정) -
git clone ssh 주소
프로젝트 경로에 clone 을 할때 ssh 주소로 가져옴
git clone git@github.com:nnyang0110/cat.git
3) 프로젝트 설정
프로젝트의 기본 경로는 /usr/local/web 이다
cd /usr/local/web
실제 운영환경 설정파일
vi ./cutelovelycat/src/main/resources/application.yml
application.yml
spring:
devtools:
livereload:
enabled: true
restart:
enabled: true
thymeleaf:
cache: false
datasource:
url: jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Seoul
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver
security:
oauth2:
client:
registration:
kakao:
client-id:
redirectUri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
client-authentication-method: POST
authorization-grant-type: authorization_code
scope: profile_nickname, account_email
client-name: Kakao
provider:
kakao:
authorization_uri: https://kauth.kakao.com/oauth/authorize
token_uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user_name_attribute: kakao_account
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
무중단 배포를 위한 설정파일
→ profiles 에 따라 포트를 다르게 줌
vi ./config/real-application.yml
real-application.yml
---
spring:
profiles: set1
server:
port: 8081
---
spring:
profiles: set2
server:
port: 8082
4) 무중단 배포 설정
기본 경로는 /usr/local/web/nonstop 이다
cd /usr/local/web/nonstop
.
├── cutelovelycat
│ └── build
│ └── libs
│ └── cat-0.0.1-SNAPSHOT.jar
├── deploy.sh
├── jar
│ ├── cat-0.0.1-SNAPSHOT.jar
│ ├── set1-cat.jar -> /usr/local/web/nonstop/jar/cat-0.0.1-SNAPSHOT.jar
│ └── set2-cat.jar -> /usr/local/web/nonstop/jar/cat-0.0.1-SNAPSHOT.jar
├── nohup.out
└── switch.sh
자동 배포, 무중단 배포 스크립트
deploy.sh
#!/bin/bash
REPOSITORY=/usr/local/web
DEPLOY_PATH=$REPOSITORY/nonstop/cutelovelycat/build/libs/
cd $REPOSITORY/cutelovelycat/
echo "> Git Pull"
git pull
echo "> 프로젝트 Build 시작"
./gradlew build
echo "> 배포 경로에 복사"
JAR_NAME=$(ls $REPOSITORY/cutelovelycat/build/libs/ |grep 'cat' |grep 'SNAPSHOT.jar' | tail -n 1)
echo "> JAR Name: $JAR_NAME"
cp ./build/libs/$JAR_NAME $DEPLOY_PATH
BASE_PATH=/usr/local/web/nonstop
BUILD_PATH=$(ls $BASE_PATH/cutelovelycat/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 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
# Nginx 에 연결되어 있지 않은 Profile 찾기
# set1 이 구동중이면 set2 를 연결
# 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
# 미연결된 Jar 로 신규 Jar 심볼릭 링크 (ln)
echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-cat.jar
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION
ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH
# Nginx 와 연결되지 않은 Profile 종료
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
# Nginx 와 연결된 Profile Jar 로 실행
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/profile"
sleep 10
for retry_count in {1..10}
do
response=$(curl -s http://localhost:$IDLE_PORT/profile)
up_count=$(echo $response | grep 'set' | wc -l)
if [ $up_count -ge 1 ]
then # $up_count >= 1 ("set" 문자열이 있는지 검증)
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
/usr/local/web/nonstop/switch.sh
동적 프록시 환경이 구축된 Nginx 가 배포 시점에 바라보는 Profile 을 자동으로 변경
하는 switch 스크립트 생성
switch.sh
#!/bin/bash
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/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/profile)
echo "> Nginx Current Proxy Port: $PROXY_PORT"
echo "> Nginx Reload"
sudo service nginx reload
참고
gradlew 의 실행권한이 없다는 메시지가 뜬다면
chmod +x ./gradlew
nginx proxy 오류 (포트 설정에 대한 부분)
→ SELinux 가 차단하여 발생
/usr/sbin/setsebool httpd_can_network_connect true
결과적으로 아래와 같이 두개의 jar 파일이 돌고 있고
현재 set2 가 실행 중이다
코드가 수정되고 배포를 하게 되면 set1 이 실행되게 된다.
Reference
스프링 부트와 AWS로 혼자 구현하는 웹서비스