코딩하는 문과생
[Spring Boot] CI/CD - CD(지속적 배포)(step3) 본문
※ 해당 글은 튜토리얼이 아닌 헷갈리거나 중요한 개념 위주로 정리한 글입니다.
[Nginx를 통한 무중단배포]
[Nginx와 기존 프로젝트 연결]
1. EC2내 엔진엑스 설치
$ sudo amazon-linux-extras install nginx1
$ sudo service nginx start
2. EC2의 보안그룹 내 포트번호 80 추가
3. 구글 로그인 및 네이버 로그인의 기존 도메인에 포트번호80 추가
4. nginx와 스프링 부트(기존프로젝트) 연동
- 프록시와 관련된 설정 추가 후 nginx 재기동
$ sudo vim /etc/nginx/nginx.conf
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;
}
- 포트번호 제거 후 url(nginx기본 포트번호:80)접속 시 정상 접속 확인
* proxy_set_header: client_ip, 도메인 등과 같이 클라이언트 정보를 실제 프로젝트로 전달할 때, header에 클라이언트 정보를 담는다.
[ProfileController를 이용한 현재 구동되는 Jar파일 찾기]
1. ProfileController 작성
- 현재 어떤 profile을 가지고 jar파일을 구동시키고 있는지 체크하기 위해 ProfileController를 작성한다.
package com.sijune.project.springboot.web;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RequiredArgsConstructor
@RestController
public class ProfileController {
private final Environment env;
@GetMapping("/profile")
public String profile() {
// 활성화된 profile을 담는다.
List<String> profiles = Arrays.asList(env.getActiveProfiles());
//filter로 사용될 profile 생성
List<String> realProfiles = Arrays.asList("real", "real1", "real2");
//profile이 없다면 "default"를, 있다면 첫 profile을 가져온다.
String defaultProfile = profiles.isEmpty()?"default":profiles.get(0);
//filter로 사용한 profile만 걸러낸다.
//filter로 사용된 profile내 존재하지 않지만, profile 내 데이터가 있다. => 제일 첫번째 profile
//filter로 사용된 profile내 존재하지 않지만, profile 내 데이터가 없다. => default
return profiles.stream().filter(realProfiles::contains).findAny().orElse(defaultProfile);
}
}
* 테스트 코드도 작성해 테스트 성공시 다음을 진행한다.
2. 각 jar파일에 해당하는 profile 작성
- 포트번호 8081로 real1, 포트번호 80802로 real2 profile을 작성한다.
-application-real1.properties
server.port=8081
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc
-application-real2.properties
server.port=8082
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc
3. 프록시 설정이 교체될 수 있도록 설정 추가
- service-url.inc 파일 내 현재 프록시 서버(nginx)가 바라보고 있는 포트번호를 저장한다.
set $service_url http://127.0.0.1:8080;
* 포트번호는 배포스크립트를 통해 변경될 예정
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;
}
* 자바의 import 느낌이다.
* 배포 시, 스크립트를 통해 $service_url이 변경되며 프록시서버가 가리키는 JAR 파일이 변경된다.
4. nginx restart로 재기동을 한다. 아직까진 포트 8080을 바라보고 있다.
[무중단 배포, Nginx와 포트번호 8081, 8082연결]
1. 배포 설정
- appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step3/zip/
overwrite: yes
permissions: # codedeploy에서 ec2로 넘어온 모든파일에 권한 부여
- object:
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
AfterInstall:
- location: stop.sh
timeout: 60
runas: ec2-user
ApplicationStart: # ec2-ussr권한으로 deploy.sh실행
- location: start.sh
timeout: 60
runas: ec2-user
ValidateService:
- location: health.sh
timeout: 60
runas: ec2-user
2. 배포스크립트 작성
- stop.sh: 엔진엑스와 연결되지 않은 스프링 부트 종료
- start.sh: 신규버전 프로젝트를 종료한 profile로 실행
- health.sh: start.sh가 정상적으로 실행되는 지 체크
- switch.sh: 엔진엑스가 바라보는 스프링부트를 최신버전으로 변경
- profile.sh: profile과 포트 체크로직
-profile.sh
#!/usr/bin/env bash
# 쉬고 있는 profile 찾기
function find_idle_profile() {
RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
# 현재 nginx가 바라보고 있는 profile찾기
if [ ${RESPONSE_CODE} -ge 400 ]; then
CURRENT_PROFILE=real2
else
CURRENT_PROFILE=$(curl -s http://localhost/profile)
fi
# 쉬고 있는 profile찾기
if [ ${CURRENT_PROFILE} == real1 ]; then
IDLE_PROFILE=real2
else
IDLE_PROFILE=real1
fi
echo "${IDLE_PROFILE}"
}
function find_idle_port() {
IDLE_PROFILE=$(find_idle_profile)
if [ ${IDLE_PROFILE} == real1 ]; then
echo "8081"
else
echo "8082"
fi
}
-stop.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh # 자바 import와 동일
#IDLE_PORT에서 쉬고 구동 중인 애플리케이션이 있다면 끈다.
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
-start.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh # 자바 import와 동일
REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=blogProject
echo "> 빌드 파일 복사"
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,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties -Dspring.profiles.active=$IDLE_PROFILE $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
-health.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh # 자바 import와 동일
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) # 현재 profile
UP_COUNT=$(echo ${RESPONSE} | grep 'real' | wc -l)
if [ ${UP_COUNT} -ge 1 ]; then # "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 "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
fi
echo "> Health check 연결 실패. 재시도..."
sleep 10
done
-switch.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh # 자바 import와 동일
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 #tee: 파일에도 쓰고 표준출력도 한다.
echo "> 엔진엑스 Reload"
sudo service nginx reload # restart는 끊김이 있지만 reload는 끊김이 없다.
}
3. git push 시 중단없이 배포되는 지 확인
: 배포마다 profile real1, real2 가 변경되며, 사용자 입장에서는 서비스를 무중단으로 이용이 가능하다.
[정리]
1. git push 시 travis CI서버가 git pull과 gradlew을 실행하며 빌드파일을 만들고
2. 빌드파일과 배포에 필요한 스크립트 파일을 AWS S3로 압축해 보낸다.
3. 그리고 저장된 압축파일을 배포를 위해 AWS Codedeploy에 설정된 배포그룹과 애플리케이션으로 보낸다.
4. AWS Codedeploy로 전달된 파일 중 appspec.yml에서 선택된 파일은 해제되어 /app/step3/zip에 저장된다.
5. 이후 appspec.yml에 형식에 맞게 배포스크립트가 동작하며 배포가 진행된다.
※ 해당 글은 "스프링 부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 저)"를 참고해 작성하였습니다.
'웹 프로그래밍 > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring AOP, 예외 처리 (0) | 2023.12.20 |
---|---|
[Spring Boot] Spring Data JPA (0) | 2021.05.01 |
[Spring boot] CI/CD - CI(지속적통합)(step2) (0) | 2020.12.12 |
[Spring boot] 프로젝트 AWS에 올리기(step1) (0) | 2020.12.10 |
[Spring Boot] AWS 아키텍처(+ HotSpot으로 연결 시 여러 이슈들) (0) | 2020.12.06 |