diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..921f52d --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,63 @@ +name: Java CI/CD with Gradle and AWS CodeDeploy + +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew clean build -x test + + - name: Prepare artifacts for deployment + run: | + mkdir -p before-deploy + cp scripts/*.sh before-deploy/ + cp appspec.yml before-deploy/ + cp build/libs/*.jar before-deploy/ + cd before-deploy && zip -r before-deploy * + cd ../ && mkdir -p deploy + mv before-deploy/before-deploy.zip deploy/walkmate.zip + + - name: Deploy to S3 (GitHub Artifacts) + uses: actions/upload-artifact@v2 + with: + name: deploy + path: deploy + + - name: AWS 자격증명 설정 + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: S3에 배포 + run: aws s3 cp deploy/walkmate.zip s3://likelion-build-hy/walkmate.zip + + - name: AWS CodeDeploy를 사용한 배포 + run: | + aws deploy create-deployment \ + --application-name walkmate-deploy \ + --deployment-group-name walkmate-deploy-group \ + --s3-location bucket=likelion-build-hy,key=walkmate.zip,bundleType=zip \ + --region ap-northeast-2 diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..6b0ff76 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,26 @@ +version: 0.0 #CodeDeploy 버전을 이야기합니다. 무조건 0.0으로 고정합니다. +os: linux +files: + - source: / #destination으로 이동시킬 파일. 여기서는 전체 파일을 의미. + destination: /home/ec2-user/app/zip/ #source에서 지정된 파일을 받는 위치 + overwrite: yes #기존 파일들을 덮어쓸지 여부 + +permissions: #CodeDeploy에서 EC2 서버로 넘겨준 파일들을 모두 ec2-user 권한을 갖도록 합니다. + - object: / + pattern: "**" + owner: ec2-user + group: ec2-user + +hooks: + AfterInstall: + - location: stop.sh #엔진엑스와 연결되어 있지 않은 스프링 부트를 종료합니다. + timeout: 60 #스크립트 60초 이상 수행되면 실패합니다. + runas: ec2-user #stop.sh를 ec2-user 권한으로 실행하게 합니다. + ApplicationStart: + - location: start.sh #엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작합니다. + timeout: 60 + runas: ec2-user + ValidateService: + - location: health.sh #새 스프링 부트가 정상적으로 실행됐는지 확인합니다. + timeout: 60 + runas: ec2-user \ No newline at end of file diff --git a/scripts/health.sh b/scripts/health.sh new file mode 100644 index 0000000..41d33cb --- /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 ] # Nginx와 연결되지 않은 포트로 스프링 부트가 잘 실행되었는지 체크합니다 + 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 0000000..7b212b8 --- /dev/null +++ b/scripts/profile.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# 쉬고 있는 profile 찾기: real1이 사용 중이면 real2가 쉬고 있고 반대면 real1이 쉬고 있음 + +function find_idle_profile() { + RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://cookie-house.store/profile) # 현재 Nginx가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인하고 응답값으로 상태코드를 전달받음 + + if [ ${RESPONSE_CODE} -ge 400 ] # 400번대 이상의 오류일 경우 real2를 사용 + then + CURRENT_PROFILE=real2 + else + CURRENT_PROFILE=$(curl -s https://hayeongyou.shop/profile) + fi + + if [ ${CURRENT_PROFILE} == real1 ] + then + IDLE_PROFILE=real2 + else + IDLE_PROFILE=real1 + fi + + echo "${IDLE_PROFILE}" # bash 스크립트는 반환 기능이 없기 때문에 echo로 값을 출력하고 클라이언트에서 그 값을 잡아서 사용 +} + +# 쉬고 있는 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 new file mode 100644 index 0000000..d0e2f19 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +ABSPATH=$(readlink -f $0) +ABSDIR=$(dirname $ABSPATH) +source ${ABSDIR}/profile.sh + +REPOSITORY=/home/ec2-user/app + +echo "> Build 파일을 복사합니다." + +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 + +IDLE_PROFILE=$(find_idle_profile) + +echo "> 새 애플리케이션을 $IDLE_PROFILE 로 실행합니다." + +# 설정 파일의 위치를 지정하고 active profile을 통해 구동될 포트를 지정합니다. +nohup java -jar \ +-Dspring.config.location=$REPOSITORY/config/application.yml,\ +$REPOSITORY/config/application-prod.yml,\ +$REPOSITORY/config/application-$IDLE_PROFILE.yml \ +-Dspring.profiles.active=$IDLE_PROFILE,prod \ +$JAR_NAME > $REPOSITORY/nohup.out 2>&1 & \ No newline at end of file diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100644 index 0000000..53b8163 --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +ABSPATH=$(readlink -f $0) # 현재 stop.sh가 속해 있는 경로를 찾습니다. + +ABSDIR=$(dirname $ABSPATH) +source ${ABSDIR}/profile.sh # 일종의 import 구문으로 stop.sh에서도 profile.sh의 function을 사용할 수 있게 합니다. + +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 -9 $IDLE_PID" + kill -9 ${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 0000000..f4f5aba --- /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://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc # 엔진엑스가 변경할 프록시 주소를 생성하여 service-url.inc로 덮어 씁니다. + + echo "> 엔진엑스 Reload" + sudo systemctl reload nginx +} \ No newline at end of file diff --git a/src/main/resources/application-real1.yml b/src/main/resources/application-real1.yml new file mode 100644 index 0000000..54b155f --- /dev/null +++ b/src/main/resources/application-real1.yml @@ -0,0 +1,2 @@ +server: + port: 8081 \ No newline at end of file diff --git a/src/main/resources/application-real2.yml b/src/main/resources/application-real2.yml new file mode 100644 index 0000000..0884131 --- /dev/null +++ b/src/main/resources/application-real2.yml @@ -0,0 +1,2 @@ +server: + port: 8082 \ No newline at end of file diff --git a/src/main/resources/config.tgz b/src/main/resources/config.tgz new file mode 100644 index 0000000..352821c Binary files /dev/null and b/src/main/resources/config.tgz differ