기록

CICD 파이프라인 구축기(3) - 애플리케이션을 EC2에 배포하기 본문

DevOps

CICD 파이프라인 구축기(3) - 애플리케이션을 EC2에 배포하기

youngyin 2024. 9. 23. 15:00

시작하면서

현재 프로젝트는 하나의 서버에서 PRD와 DEV 환경의 서비스를 운영하고 있으며, 향후 사용자가 증가할 경우 인스턴스를 추가하여 블루그린 배포 방식으로 전환할 계획입니다.

- 이전글1 : 2024.08.16 - [DevOps] - CICD 파이프라인 구축기(1) - 브런치 전략과 GITACTION

- 이전글2 : 2024.08.24 - [DevOps] - CICD 파이프라인 구축기(2) - Verification Job과 Secrets 관리

 

아래는 Git Action과 배포 스크립트를 사용해서 만든 간단한 배포 프로세스 입니다.

배포 방식의 선택: 재배포 vs. 블루그린 배포

재배포 (Re-deployment)

재배포 방식은 기존 애플리케이션을 중단하고 새로운 버전을 한 번에 배포하는 방식입니다. 이 방식의 장점은 설정과 구현이 간단하다는 점입니다. 단일 서버에서 모든 작업이 이루어지므로, 별도의 인프라 설정이 필요 없습니다. 또한, 작은 규모의 프로젝트나 단순한 서비스에 적합합니다.
하지만 단점으로는 배포 중에 다운타임이 발생할 수 있다는 점입니다. 사용자가 서비스를 이용하고 있을 때 새로운 버전으로 교체하는 경우, 일정 시간 동안 서비스가 중단될 수 있습니다. 이로 인해 사용자 경험이 저하될 수 있으며, 비즈니스에 부정적인 영향을 미칠 수 있습니다.

블루그린 배포 (Blue-Green Deployment)

블루그린 배포는 두 개의 환경(블루와 그린)을 사용하여 애플리케이션을 배포하는 방식입니다. 현재 운영 중인 버전이 블루 환경에 위치하고, 새로운 버전이 그린 환경에 배포됩니다. 새로운 버전이 안정적이라고 판단되면, 트래픽을 블루에서 그린으로 전환합니다. 이 과정에서 몇 가지 장점이 있습니다:

  • 다운타임 최소화: 새로운 버전이 완전히 준비된 후에만 트래픽을 전환하므로 서비스 중단이 없습니다.
  • 롤백 용이성: 문제가 발생할 경우, 즉시 블루 환경으로 트래픽을 되돌릴 수 있어 안정성이 높습니다.
  • 테스트 환경: 새로운 버전을 별도의 환경에서 충분히 테스트한 후 배포할 수 있습니다.

프로젝트 구조

현재 프로젝트는 AWS EC2 인스턴스를 활용하여 운영되고 있으며, 하나의 인스턴스에서 개발 환경(DEV)과 운영 환경(PRD)을 모두 호스팅하고 있습니다. 이러한 구조는 초기 단계에서 비용 효율성과 관리의 용이성을 고려한 선택입니다.

하나의 EC2 인스턴스 내에서 개의 환경을 구분하기 위해, 환경에 필요한 애플리케이션 설정과 리소스를 명확히 분리하여 구성했습니다. DEV 환경은 개발과 테스트를 위한 설정을 포함하고 있으며, PRD 환경은 실제 사용자에게 제공되는 안정적인 서비스를 위한 설정을 갖추고 있습니다.

 

이러한 점을 고려했을 때, 블루그린 배포는 구현이 복잡하고 초기 인프라 비용이 증가할 수 있습니다. AWS의 CodeDeploy와 같은 도구를 사용하여 자동화할 경우 추가적인 설정이 필요해서, 재배포처럼 단순한 프로세스를 선택했습니다.

배포 프로세스와 스크립트

현재 배포 프로세스는 코드 변경 사항을 JAR 파일로 만들어 EC2 인스턴스에 옮긴 후, 배포 스크립트를 통해 자동으로 실행되도록 설정하고 있습니다.

아래는 GitHub Actions를 통해 정의한 배포 과정의 YAML 코드입니다:

더보기
deploy:
    runs-on: ubuntu-latest
    needs: verification
    if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/prd')
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          ref: ${{ github.ref }}

      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'

      - name: Create aws-info file
        run: |
          echo "${{ secrets.RDS_INFO_V1 }}" > src/main/resources/aws-info.yml

      - name: Create JAR file
        run: |
          chmod +x gradlew
          ./gradlew clean bootJar
          mkdir -p build/libs
          cp build/libs/*.jar apiProject.jar

      - name: Backup jar on remote server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            TIMESTAMP=$(date +'%Y%m%d%H%M%S')
            if [ -f bin/apiProject.jar ]; then
              cp -a bin/apiProject.jar backup/apiProject_$TIMESTAMP.jar
              echo "Completed backup of jar to backup/apiProject_$TIMESTAMP.jar"
            else
              echo "No jar file found to backup"
            fi

      - name: Copy jar to remote server
        uses: appleboy/scp-action@v0.1.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          target: bin
          source: apiProject.jar
          overwrite: true
  1. Checkout Code: 코드 변경 사항을 가져옵니다. 최신 버전의 코드를 확보하는 단계입니다.
  2. Set up JDK 17: Java 환경을 설정합니다. 애플리케이션 실행에 필요한 JDK를 준비합니다.
  3. Create aws-info file: AWS 연결 정보를 설정합니다. 민감한 정보는 GitHub Secrets에서 안전하게 불러옵니다.
  4. Create JAR file: Gradle을 사용하여 애플리케이션을 빌드하고 JAR 파일을 생성합니다.
  5. Backup jar on remote server: 기존 JAR 파일을 백업합니다. 문제가 발생할 경우를 대비한 안전망입니다.
  6. Copy jar to remote server: 새로 생성한 JAR 파일을 원격 서버에 전송합니다.

배포가 완료된 후, 다음 단계에서 애플리케이션을 실행하는 프로세스가 있습니다:

더보기
run-dev:
    runs-on: ubuntu-latest
    needs: deploy
    if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
    steps:
      - name: Execute deploy script on remote server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            if [ -f bin/deploy.sh ]; then
              echo "deploy.sh found, executing script..."
              bin/deploy.sh dev
            else
              echo "deploy.sh not found in bin directory"
              exit 1
            fi

배포 스크립트의 실행

배포 스크립트를 실행하여 새로운 버전의 애플리케이션을 시작합니다. 

더보기
if [ -z "$ENVIRONMENT" ]; then
  echo "Usage: $0 {local|dev|prd}"
  exit 1
fi

PID_FILE="/home/bin/pidInfo/${ENVIRONMENT}.pid"
JAR_FILE="/home/bin/apiProject.jar"
PROFILE=$ENVIRONMENT

# 기존 프로세스 종료
if [ -f "$PID_FILE" ]; then
  PID=$(cat "$PID_FILE")
  if ps -p $PID > /dev/null; then
    echo "Stopping existing process $PID"
    kill -9 $PID
    echo "Process $PID stopped"
  else
    echo "No running process found with PID $PID"
  fi
  sudo rm -f "$PID_FILE"
else
  echo "No PID file found. Skipping process termination."
fi

# 새로운 프로세스 시작
echo "Starting new process with profile $PROFILE"
nohup java -jar -Dspring.profiles.active=$PROFILE "$JAR_FILE" > /home/logs/dev/$PROFILE.log 2>&1 &
NEW_PID=$!
echo $NEW_PID > "$PID_FILE"
echo "New process started with PID $NEW_PID"

백업 파일 관리의 필요성

배포 중에 문제가 발생할 경우를 대비하여 기존 JAR 파일을 백업하는 단계를 추가했습니다. 하지만 백업 파일이 많아지면서 관리의 필요성을 느꼈습니다. 실제로 로그 파일이나 백업 파일이 과도하게 증가하여 용량 문제를 야기한 경험이 있었습니다.
이 문제를 해결하기 위해 두 가지 방법을 고려했습니다:

  1. 주기적 백업 파일 삭제: 인스턴스에 서비스를 등록하여 일주일 주기로 백업 파일을 자동으로 삭제하는 방법입니다. 이 방법은 관리의 부담을 덜 수 있지만, 설정이 다소 복잡할 수 있습니다.
  2. 최근 백업 파일 유지: 새로운 백업 파일을 생성할 때마다, 일정 개수의 최근 파일만 남기고 나머지는 삭제하는 방법입니다. 이 방법은 더 손쉬운 관리가 가능할 것 같아 현재 적용을 고려 중입니다.
Comments