diff --git a/.github/workflows/cicd_production.yml b/.github/workflows/cicd_production.yml index 201b7151..84229c6c 100644 --- a/.github/workflows/cicd_production.yml +++ b/.github/workflows/cicd_production.yml @@ -55,6 +55,15 @@ jobs: gradle-version: 8.7 arguments: clean build -x test + # 실제 필요한 파일(Jar, appspect.yml, 배포 스크립트)만 담기 + - name: zip 파일 생성 + run: | + mkdir -p deploy/scripts + cp scripts/*.sh deploy/scripts + cp appspec.yml deploy/ + cp build/libs/*.jar deploy/ + cd deploy && zip -r ./$GITHUB_SHA.zip * + shell: bash # (5) AWS 인증 (IAM 사용자 Access Key, Secret Key 활용) - name: Configure AWS credentials @@ -68,11 +77,8 @@ jobs: # (6) 빌드 결과물을 S3 버킷에 업로드 - name: Upload to AWS S3 run: | - aws deploy push \ - --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \ - --ignore-hidden-files \ - --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \ - --source . + aws s3 cp deploy/$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip + # (7) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행 - name: Deploy to AWS EC2 from S3 diff --git a/appspec.yml b/appspec.yml index 12923539..011e1345 100644 --- a/appspec.yml +++ b/appspec.yml @@ -3,7 +3,7 @@ os: linux files: - source: / - destination: /home/ubuntu/app + destination: /home/ubuntu/app/step3/zip/ overwrite: yes permissions: @@ -14,11 +14,14 @@ permissions: hooks: AfterInstall: - - location: scripts/stop.sh + - location: scripts/stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료합니다. timeout: 60 runas: ubuntu ApplicationStart: - - location: scripts/start.sh + - location: scripts/start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작합니다. + timeout: 60 + runas: ubuntu + ValidateService: + - location: scripts/health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인합니다. timeout: 60 runas: ubuntu - diff --git a/build.gradle b/build.gradle index d045f4ba..44f07282 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'com.nawabali' -version = '0.0.1-SNAPSHOT' +version = '1.0.1-SNAPSHOT-' + new Date().format("yyyy-MM-dd-HHmmss") java { sourceCompatibility = '17' @@ -103,7 +103,7 @@ test { useJUnitPlatform() } -//// 빌드 시 plain jar 파일은 만들어지지 않기 위해 추가 -//jar { -// enabled = false -//} \ No newline at end of file +// plain jar 생성되지 않게 설정 +jar { + enabled = false +} \ No newline at end of file diff --git a/scripts/health.sh b/scripts/health.sh new file mode 100644 index 00000000..f74b9ce7 --- /dev/null +++ b/scripts/health.sh @@ -0,0 +1,39 @@ +#!/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://127.0.0.1:$IDLE_PORT/profile " +sleep 10 + +for RETRY_COUNT in {1..10} +do + RESPONSE=$(curl -s http://127.0.0.1:${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 \ No newline at end of file diff --git a/scripts/profile.sh b/scripts/profile.sh new file mode 100644 index 00000000..9706c0f9 --- /dev/null +++ b/scripts/profile.sh @@ -0,0 +1,38 @@ +#!/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://127.0.0.1/profile) + + if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함) + then + CURRENT_PROFILE=real2 + else + CURRENT_PROFILE=$(curl -s http://127.0.0.1/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 +} \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh index 51663229..d3cb5b04 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,21 +1,42 @@ #!/usr/bin/env bash -PROJECT_ROOT="/home/ubuntu/app" -JAR_FILE="$PROJECT_ROOT/build/libs/nawabali-0.0.1-SNAPSHOT.jar" +ABSPATH=$(readlink -f $0) +ABSDIR=$(dirname $ABSPATH) +source ${ABSDIR}/profile.sh -APP_LOG="$PROJECT_ROOT/application.log" -ERROR_LOG="$PROJECT_ROOT/error.log" -DEPLOY_LOG="$PROJECT_ROOT/deploy.log" +REPOSITORY=/home/ubuntu/app/step3 +PROJECT_NAME=ZeroDowntimeDeployment +DEPLOY_LOG="$REPOSITORY/deploy.log" TIME_NOW=$(date +%c) -# build 파일 복사 -echo "$TIME_NOW > $JAR_FILE 파일 복사" >> $DEPLOY_LOG -cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE +echo "> Build 파일 복사" +echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/" -# jar 파일 실행 -echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG -nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG & +cp $REPOSITORY/zip/*.jar $REPOSITORY/ -CURRENT_PID=$(pgrep -f $JAR_FILE) -echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG +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 로 실행합니다." + +# 쉬고 있던 프로필로 jar파일을 백그라운드 실행 +nohup java -jar \ + -Dspring.config.location="classpath:/application.properties, classpath:/application-$IDLE_PROFILE.properties" \ + -Dspring.profiles.active=$IDLE_PROFILE \ + $JAR_NAME > $REPOSITORY/application.log 2>&1 & + +# Deploy 로그 +echo "$TIME_NOW > $JAR_NAME 파일 실행" >> $DEPLOY_LOG +CURRENT_PID=$(pgrep -f $JAR_NAME) +echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG \ No newline at end of file diff --git a/scripts/stop.sh b/scripts/stop.sh index e4e87401..fcf1f1b0 100644 --- a/scripts/stop.sh +++ b/scripts/stop.sh @@ -1,19 +1,23 @@ #!/usr/bin/env bash -PROJECT_ROOT="/home/ubuntu/app" -JAR_FILE="$PROJECT_ROOT/build/libs/nawabali-0.0.1-SNAPSHOT.jar" - -DEPLOY_LOG="$PROJECT_ROOT/deploy.log" +ABSPATH=$(readlink -f $0) +ABSDIR=$(dirname $ABSPATH) +source ${ABSDIR}/profile.sh +IDLE_PORT=$(find_idle_port) TIME_NOW=$(date +%c) +DEPLOY_LOG="/home/ubuntu/app/step3/deploy.log" -# 현재 구동 중인 애플리케이션 pid 확인 -CURRENT_PID=$(pgrep -f $JAR_FILE) +echo "> $IDLE_PORT 에서 구동중인 애플리케이션 pid 확인" +IDLE_PID=$(lsof -ti tcp:${IDLE_PORT}) -# 프로세스가 켜져 있으면 종료 -if [ -z $CURRENT_PID ]; then +if [ -z ${IDLE_PID} ] +then echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG + echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." else - echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG - kill -15 $CURRENT_PID -fi + echo "$TIME_NOW > 실행중인 $IDLE_PID 애플리케이션 종료 " >> $DEPLOY_LOG + echo "> kill -15 $IDLE_PID" + kill -15 ${IDLE_PID} + sleep 5 +fi \ No newline at end of file diff --git a/scripts/switch.sh b/scripts/switch.sh new file mode 100644 index 00000000..b8054928 --- /dev/null +++ b/scripts/switch.sh @@ -0,0 +1,16 @@ +#!/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://15.165.121.186:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc + + echo "> 엔진엑스 Reload" + sudo service nginx reload +} \ No newline at end of file diff --git a/src/main/java/com/nawabali/nawabali/HelloController.java b/src/main/java/com/nawabali/nawabali/HelloController.java index 1cf914e3..28a8a82c 100644 --- a/src/main/java/com/nawabali/nawabali/HelloController.java +++ b/src/main/java/com/nawabali/nawabali/HelloController.java @@ -7,6 +7,6 @@ public class HelloController { @GetMapping("/ping") public String check() { - return "Pong!"; + return "Pong! Zero-Downtime deployment succeeded"; } } diff --git a/src/main/java/com/nawabali/nawabali/ProfileController.java b/src/main/java/com/nawabali/nawabali/ProfileController.java new file mode 100644 index 00000000..36b58ddb --- /dev/null +++ b/src/main/java/com/nawabali/nawabali/ProfileController.java @@ -0,0 +1,27 @@ +package com.nawabali.nawabali; + +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() { + // 현재 실행 중인 ActiveProfile을 모두 가져옵니다. + List profiles = Arrays.asList(env.getActiveProfiles()); + List realProfiles = Arrays.asList("real1", "real2"); + String defaultProfile = profiles.isEmpty()? "default" : profiles.get(0); + return profiles.stream() + .filter(realProfiles::contains) + .findAny() + .orElse(defaultProfile); + } +} \ No newline at end of file diff --git a/src/main/java/com/nawabali/nawabali/config/WebSecurityConfig.java b/src/main/java/com/nawabali/nawabali/config/WebSecurityConfig.java index 0e536c65..373abe83 100644 --- a/src/main/java/com/nawabali/nawabali/config/WebSecurityConfig.java +++ b/src/main/java/com/nawabali/nawabali/config/WebSecurityConfig.java @@ -99,7 +99,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/").permitAll() // 메인 페이지 요청 허가 .requestMatchers("/users/logout").permitAll() .requestMatchers("/main.html").permitAll() // 메인 html페이지 요청 허가 - .requestMatchers("/ping").permitAll() // 항상 200 OK 반환하는 health check 전용 API + .requestMatchers("/ping", "/profile").permitAll() // 항상 200 OK 반환하는 health check 전용 API .requestMatchers("/users/signup").permitAll() .requestMatchers(HttpMethod.POST, "/users/login").permitAll() .requestMatchers("/posts").permitAll()