Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 무중단배포 적용 #108

Merged
merged 4 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .github/workflows/cicd_production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 7 additions & 4 deletions appspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ os: linux

files:
- source: /
destination: /home/ubuntu/app
destination: /home/ubuntu/app/step3/zip/
overwrite: yes

permissions:
Expand All @@ -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

10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -103,7 +103,7 @@ test {
useJUnitPlatform()
}

//// 빌드 시 plain jar 파일은 만들어지지 않기 위해 추가
//jar {
// enabled = false
//}
// plain jar 생성되지 않게 설정
jar {
enabled = false
}
39 changes: 39 additions & 0 deletions scripts/health.sh
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions scripts/profile.sh
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 34 additions & 13 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -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
26 changes: 15 additions & 11 deletions scripts/stop.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions scripts/switch.sh
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion src/main/java/com/nawabali/nawabali/HelloController.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
public class HelloController {
@GetMapping("/ping")
public String check() {
return "Pong!";
return "Pong! Zero-Downtime deployment succeeded";
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/nawabali/nawabali/ProfileController.java
Original file line number Diff line number Diff line change
@@ -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<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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down