무중단 배포하기

Jang990·2025년 6월 20일
0

BookSpot

목록 보기
8/16

Spring 서버를 EC2에 올리는데 Github Action, S3, CodeDeploy를 활용해서 EC2에 무중단 배포를 해볼 것이다.

  • Github Action
    • Java 프로젝트 빌드(테스트 없이)
    • 배포에 필요한 파일들을(sh파일, 빌드 결과 jar 파일, appspec.yml)을 S3에 압축해서 업로드,
    • CodeDeploy에게 특정 배포 그룹 배포 요청
  • CodeDeploy
    • S3에서 배포에 필요한 파일들을 가져옴
    • appspec.yml에 따라 배포 처리

EC2 설정

Nginx 설치

-- nginx 설치
sudo yum install nginx

-- nginx 시작
sudo service nginx start

-- 엔진엑스 설정이 모여있는 폴더에 service-url.inc 파일 생성
sudo vim /etc/nginx/conf.d/service-url.inc

	# service-url.inc 파일 내용
	set $service_url http://127.0.0.1:8080;
    
    
-- nginx 설정 변경 = 스프링 연동
sudo vim /etc/nginx/nginx.conf

    # nginx.conf 파일 내용
    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;
    }


-- nginx 재시작
sudo service nginx restart

ec2 IP의 80포트로 요청을 보내서 spring 서버에서 응답이 오는지 확인하고 정상적인 응답이 오면 성공

CodeDeploy Agent 설치

sudo yum update
sudo yum install ruby -y
sudo yum install wget -y

# 홈 디렉터리 이동 
cd /home/ec2-user

wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install

sudo ./install auto > /tmp/logfile

# 설치 확인
sudo systemctl status codedeploy-agent

만약 CodeDeploy로 배포 중 오류가 발생했다면
EC2에서 /opt/codedeploy-agent/deployment-root/deployment-logs 이 경로에 있는 로그들을 확인해보길 바란다.

CodeDeploy + S3 + Github-Action 설정

CodeDepoly와 Github-Action 생성 밑 설정부분은 다음 링크를 참고하자.

링크한 글은 무중단 배포가 아닌 배포 자동화를 다루기 때문에 appspec.yml이나 workflow, 스크립트 들은 다르다.
그러니 CodeDeploy, IAM, S3, Github-Action 키 설정 정도만 따라하면 된다.

velog/juhyeon1114/실전! Github actions, AWS Code deploy로 Spring boot 배포 자동화하기

Spring 프로젝트 코드 추가

Spring 프로젝트에 배포를 위한 코드들을 추가해주자.

컨트롤러 작성

@RestController
@RequiredArgsConstructor
public class ProfileController {
    private static final List<String> realProfiles = List.of("real1", "real2");
    private final Environment env;

    @GetMapping("profile")
    public String profile() {
        List<String> profiles = Arrays.asList(env.getActiveProfiles());

        return profiles.stream()
                .filter(realProfiles::contains)
                .findAny()
                .orElseGet(() -> profiles.isEmpty() ? "default" : profiles.getFirst());
    }
}

yml 파일 작성

8081포트와 8082 포트로 서버를 올릴 것이다.
application-real1.ymlapplication-real2.yml파일을 다음과 같이 생성한다.

# real1
server.port: 8081
spring.profiles.include: "prod"
# real2
server.port: 8081
spring.profiles.include: "prod"

/home/ec2-user/appapplication-prod.yml이 있어야 한다.
(include 부분과 파일은 프로젝트에 맞게 변경하면 된다.)

스크립트 작성

https://github.com/yeon-06/springboot-aws/tree/master/scripts

여기서 deploy.sh빼고 전부 가져오자.
프로젝트 환경에 따라서 start.sh에 다음 항목만 수정해주면 될 것이다.

만약 실행이 됐는데 특정 스크립트들이 의도한 대로 동작하지 않는다면 배포를 시도한 EC2에서 /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deploy.log 로그를 확인해보자.

start.sh 수정

start.sh를 그대로 사용하면 이전 로그들이 사라진다.
나는 logs라는 디렉토리를 만들어서 이전 로그들을 보관하기로 했다.

디렉토리 구조

  • /home/ec2-user/app/project - application-prod.yml 존재
    • /zip : S3에 올라온 배포 파일을 전송 디렉토리
    • /logs : 로그 보관 디렉토리
#!/usr/bin/env bash
# 배포할 신규버전 스프링 부트 프로젝트를 stop.sh로 종료한 'profile'로 실행

# 현재 실행중이지 않은 Profile을 찾아서 실행
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH) # start.sh 경로 찾기
source ${ABSDIR}/profile.sh # import 구문

REPOSITORY=/home/ec2-user/app/project
PROJECT_NAME=bookspot

echo "> Build 파일 복사"
echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/"

cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | grep -v 'plain' | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME 에 실행권한 추가"

chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"

IDLE_PROFILE=$(find_idle_profile)

# 이전 로그 보관
CURRENT_TIME=$(date +%Y%m%d-%H%M%S)
LOG_FILENAME=deployed-at-${CURRENT_TIME}-${PROJECT_NAME}-app.log
LOG_FILE=$REPOSITORY/${LOG_FILENAME}
LOG_DIR=$REPOSITORY/logs
PREVIOUS_LOGS=$(find $REPOSITORY -maxdepth 1 -name "*-${PROJECT_NAME}-app.log")

if [ -n "$PREVIOUS_LOGS" ]; then
  echo "> 기존 로그 파일을 $LOG_DIR/ 로 이동합니다:"
  for file in $PREVIOUS_LOGS; do
    filename=$(basename $file)
    echo "  - $filename"
    mv "$file" "$LOG_DIR/"
  done
else
  echo "> 이전 로그 파일이 없습니다."
fi

echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."
nohup java -jar \
    -Dspring.config.location=classpath:/application-$IDLE_PROFILE.yml,/home/ec2-user/app/project/application-prod.yml \
    -Dspring.profiles.active=$IDLE_PROFILE \
    $JAR_NAME > ${LOG_FILE} 2>&1 &

appspec.yml 설정 (CodeDeploy)

프로젝트 루트에 다음 appspec.yml을 추가하자.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/project/zip
    overwrite: yes
hooks:
  AfterInstall:
    - location: stop.sh
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: start.sh
      timeout: 60
      runas: ec2-user
  ValidateService:
    - location: health.sh
      timeout: 60
      runas: ec2-user

destination도 프로젝트에 맞게 설정하면 된다.

workflow 설정 (Github Action)

다음 .gihhub/workflow/main.yml을 추가해주자.

name: CICD BookSpot
run-name: Running
on:
  push:
    branches:
      - main

env:
  AWS_REGION: ap-northeast-2
  AWS_S3_BUCKET: my-bookspot-bucket
  AWS_CODE_DEPLOY_APPLICATION: bookspot-CD
  AWS_CODE_DEPLOY_GROUP: bookspot-CD-group
  AWS_S3_OBJECT_NAME: bookspot-deploy

jobs:
  build-with-gradle:
    runs-on: ubuntu-latest
    steps: 
    - name: main 브랜치로 이동
      uses: actions/checkout@v3
      with:
        ref: main
    - name: JDK 21 설치
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'corretto'
    - name: gradlew에 실행 권한 부여
      run: chmod +x ./gradlew
    - name: 프로젝트 빌드
      run: ./gradlew clean build -x test

    - name: deploy 디렉토리 생성
      run: |
        mkdir deploy/ 
        cp build/libs/*.jar deploy/ 
        cp script/*.sh deploy/ 
        cp appspec.yml deploy/ 
      
    - name: AWS credential 설정
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-region: ${{ env.AWS_REGION }}
        aws-access-key-id: ${{ secrets.CICD_ACCESS_KEY }}
        aws-secret-access-key: ${{ secrets.CICD_SECRET_KEY }}
    - name: deploy 디렉토리를 S3 업로드
      run: |
        aws deploy push --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} --ignore-hidden-files \
        --s3-location s3://$AWS_S3_BUCKET/$AWS_S3_OBJECT_NAME/$GITHUB_SHA.zip \
        --source deploy
    - name: EC2 배포
      run: |
        aws deploy create-deployment --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \
        --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} \
        --s3-location bucket=$AWS_S3_BUCKET,key=$AWS_S3_OBJECT_NAME/$GITHUB_SHA.zip,bundleType=zip

이 설정도 프로젝트에 맞게 설정해주면 된다.
나는 main 브랜치에 푸시가 일어나면 적용되도록 설정했다.
env부분만 설정해주면 될 것이다.

결과

이제 브라우저로 ec2-IP:80이나 ec2-IP로 요청을 보내면 서버가 잘 동작하는 것을 확인할 수 있다.

  1. Github Action에서 Repository의 코드로 빌드 결과를 S3에 올리고 CodeDepoly에 배포 요청
  2. CodeDepoly에서 배포 스크립트 순차 실행
    • 사용하지 않는 profile(8081 or 8082 포트) 찾아서 kill 명령어 실행
    • 사용하지 않는 profile로 jar 파일 실행
    • Nginx가 $service_url을 사용하지 않는 포트로 변경 후 reload
  3. 무중단 배포 완료

첫 배포에는 하나의 Spring 서버가 올라가지만, 두번째 배포부터 8081과 8082 두 포트에 모두 Spring 서버가 올라가 있는 것이 정상이다.

profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글