[BONBON] CI/CD 파이프라인 설계, 구축, 시연 (BE)

나무나무·2025년 8월 7일

프로젝트 BonBon

목록 보기
8/10
post-thumbnail

AWS 사양

  • Front End : Route53, ACM, S3, CloudFront
  • Back End : S3, EC2, AutoScaling Group, CodeDeploy(Blue-Green)
  • Machin Learning : S3, EC2, CodeDeploy

시스템 아키텍처

  • 이번 포스트에선 BackEnd CI/CD 쪽을 구현해볼 예정이다.
  • 정리할 내용이 생각보다 많다..

BackEnd CI/CD

배포 프로세스

1️⃣ GitHub에 코드 commit & push(main 브랜치) → GitHub Actions CI 실행
2️⃣ 빌드 결과물 (.jar, appspec.yml, scripts) 압축 후 S3에 업로드
3️⃣ CodeDeployAuto Scaling Group을 이용해 Green 환경에 배포
4️⃣ Green 인스턴스 Health 체크 완료 시, BlueGreen으로 트래픽 전환
5️⃣ 배포 완료 후 Blue 인스턴스 자동 종료



GitHub Actions CI/CD 구축

🗂️ 디렉토리 구조
├─.github
│ ├─ workflows/
│ │ └─ deploy.yaml/
├─ bonbon/
│ ├─ src/
│ │ └─ main/
│ │ └─ resources/
│ ├─ build/
│ ├─ gradlew
│ ├─ build.gradle
├─ scripts/
│ └─start.sh / stop.sh / init-dir.sh (배포 스크립트)
├─ appspec.yml

백엔드 CI/CD 파이프라인

  • appspec.yml(CD) - AWS CodeDeploy가 배포 작업 중 어떤 스크립트를 어떤 타이밍에 실행할지 정의하는 설정 파일
  • start.sh(CD) - 애플리케이션 실행
  • stop.sh(CD) - 기존에 실행중이던 애플리케이션 중지
  • deploy.yaml(CI) - 배포 전 작업을 수행하는 사용자 정의 스크립트
  • init-dir.sh(CD) - EC2 인스턴스 초기 배포 시 초기 디렉토리 및 권한 설정

전체 흐름

1️⃣ [ CI 단계 ]

  • deploy.yaml
    → 코드 빌드
    → .jar 파일 생성 , zip 압축
    → 산출물 S3 업로드

1️⃣ [ CD 단계 ]

  • appspec.yml
    BeforeInstall: init-dir.sh 실행 (초기 디렉토리 준비)
    ApplicationStop: stop.sh 실행 (기존 프로세스 종료)
    ApplicationStart: start.sh 실행 (zip 다운로드, 압축 해제, .jar 실행)

deploy.yaml

  • 이 과정 지나면 빌드, 테스트 후에 압축 파일이 S3로 올라감
  • 생성되는 파일 구조는 아래와 같음

🗂️ 디렉토리 구조
bonbon/
├─ 5f6c33d.zip/
│ ├─ build/
│ ├─ appspec.yml
│ └─ scripts/
│ │ ├─ start.sh
│ │ ├─ stop.sh
│ │ ├─ init-dir.sh
│ │ └─ sha.txt

name: BonBon Back-end CI&CD

on:
  push:
    branches: main
env:
  AWS_REGION: ap-northeast-2 #리전
  S3_BUCKET_NAME: bonbon-back-bucket #버킷 이름
  CODE_DEPLOY_APPLICATION_NAME: bonbon-dev  #CodeDeploy 애플리케이션 이름
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: bonbon-dev #CodeDeploy 배포 그룹 이름

permissions:
  contents: read
  
jobs:
  build:
    # job이 돌아갈 환경
    runs-on: ubuntu-latest
    steps:
      # 기본 체크아웃
      - name: Checkout
        uses: actions/checkout@v3

      # resources 디렉토리 생성
      - name: Create resources directory (for ignored files)
        run: mkdir -p ./bonbon/bonbon/src/main/resources

      # properties들 생성
      - name: Set application.properties
        run: |
          echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./bonbon/bonbon/src/main/resources/application.properties
          echo "${{ secrets.APPLICATION_JWT_PROPERTIES }}" > ./bonbon/bonbon/src/main/resources/application-jwt.properties

      # gradlew 권한 추가
      - name: Make gradlew executable
        run: chmod +x ./bonbon/bonbon/gradlew
        
      # 빌드, 테스트
      - name: Build and Test
        run: |
          cd ./bonbon/bonbon
          ./gradlew clean build test --info

      # GitHub 커밋의 고유한 SHA 값을 scripts/sha.txt 파일에 저장 -> 어떤 커밋 배포했는지 확인할라고
      - name: Save SHA to file
        run: echo $GITHUB_SHA > ./bonbon/bonbon/scripts/sha.txt

      # zip 파일 압축(scripts, appspec.yml, build 포함)
      - name: Make deployment zip
        run: |
          cd ./bonbon/bonbon
          echo $GITHUB_SHA > scripts/sha.txt
          zip -r $GITHUB_SHA.zip build appspec.yml scripts
          mv $GITHUB_SHA.zip ../../
      
      # JDK 23 세팅
      - name: Set up JDK 23
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '23'

      # AWS 인증
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY_ID }}
          aws-region: ${{ env.AWS_REGION }}
          
      - name: Upload to S3
        run: |
          aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ env.S3_BUCKET_NAME }}/$GITHUB_SHA.zip
      
      # CodeDeploy로 배포 실행
      - name: Deploy to EC2
        run: |
          aws deploy create-deployment \
            --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
            --deployment-config-name CodeDeployDefault.AllAtOnce \
            --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
            --s3-location bucket=${{ env.S3_BUCKET_NAME }},key=${{ github.sha }}.zip,bundleType=zip

init-dir.sh

  • 디렉토리 최초 생성 및 초기화
#!/bin/bash

# 프로젝트 루트 디렉토리 경로
PROJECT_ROOT="/home/ec2-user/app"

# 디렉토리 확인 후 생성
if [ ! -d "$PROJECT_ROOT" ]; then
  echo "$(date '+%Y-%m-%d %H:%M:%S') > $PROJECT_ROOT 디렉토리 없음. 생성 중..." >> /home/ec2-user/deploy.log
  mkdir -p $PROJECT_ROOT
  echo "$(date '+%Y-%m-%d %H:%M:%S') > $PROJECT_ROOT 디렉토리 생성 완료." >> /home/ec2-user/deploy.log
else
  echo "$(date '+%Y-%m-%d %H:%M:%S') > $PROJECT_ROOT 디렉토리가 이미 존재합니다." >> /home/ec2-user/deploy.log
fi

# 디렉토리 권한 설정 (ec2-user에게 소유권 부여)
echo "$(date '+%Y-%m-%d %H:%M:%S') > $PROJECT_ROOT 디렉토리 권한 변경 중..." >> /home/ec2-user/deploy.log
chown -R ec2-user:ec2-user $PROJECT_ROOT
chmod -R 755 $PROJECT_ROOT
echo "$(date '+%Y-%m-%d %H:%M:%S') > $PROJECT_ROOT 디렉토리 권한 설정 완료." >> /home/ec2-user/deploy.log

start.sh

  • 막상 코드를 작성하다 보니 start.shstop.sh가 하는 일까지 포함시켰다..
  • CodeDeploy에서 공식적으로 분리 사용을 권장하니 분리된 구조로 추후에 바꿔야겠다
#!/usr/bin/env bash

# 프로젝트 루트 디렉토리 설정
PROJECT_ROOT="/home/ec2-user/app/build/libs"
JAR_NAME="bonbon-0.0.1-SNAPSHOT.jar"
JAR_FILE="$PROJECT_ROOT/$JAR_NAME"

# 로그 파일 경로
APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"

# 현재 시간 저장
TIME_NOW=$(date '+%Y-%m-%d %H:%M:%S')


# sha.txt에서 SHA 읽기
SHA=$(cat /home/ec2-user/app/scripts/sha.txt)

# 다운로드
aws s3 cp s3://bonbon-back-bucket/${SHA}.zip /home/ec2-user/app/${SHA}.zip

# S3에서 ZIP 파일 다운로드
echo "$TIME_NOW > S3에서 ZIP 파일 다운로드 시작" >> $DEPLOY_LOG
aws s3 cp s3://bonbon-back-bucket/${SHA}.zip $PROJECT_ROOT/${SHA}.zip >> $DEPLOY_LOG 2>&1
if [ $? -ne 0 ]; then
  echo "$TIME_NOW > 오류: S3에서 ZIP 파일 다운로드 실패" >> $DEPLOY_LOG
  exit 1
fi
echo "$TIME_NOW > ZIP 파일 다운로드 완료" >> $DEPLOY_LOG

# ZIP 파일 압축 해제
echo "$TIME_NOW > ZIP 파일 압축 해제 시작" >> $DEPLOY_LOG
unzip -o $PROJECT_ROOT/${SHA}.zip -d $PROJECT_ROOT >> $DEPLOY_LOG 2>&1
if [ $? -ne 0 ]; then
  echo "$TIME_NOW > 오류: ZIP 파일 압축 해제 실패" >> $DEPLOY_LOG
  exit 1
fi
echo "$TIME_NOW > ZIP 파일 압축 해제 완료" >> $DEPLOY_LOG

# 기존 프로세스 종료 (있을 경우)
EXISTING_PID=$(pgrep -f "$JAR_FILE")
if [ -n "$EXISTING_PID" ]; then
  echo "$TIME_NOW > 기존 프로세스 종료 (PID: $EXISTING_PID)" >> $DEPLOY_LOG
  kill -9 $EXISTING_PID
fi

# JAR 파일 실행
echo "$TIME_NOW > $JAR_FILE 파일 실행 시작" >> $DEPLOY_LOG
nohup /opt/jdk-23/bin/java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &

echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG

stop.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ec2-user/app"
JAR_FILE="$PROJECT_ROOT/spring-webapp.jar"

DEPLOY_LOG="$PROJECT_ROOT/deploy.log"

TIME_NOW=$(date '+%Y-%m-%d %H:%M:%S')

# 현재 구동 중인 애플리케이션 pid 확인
CURRENT_PID=$(pgrep -f $JAR_FILE)

# 프로세스가 켜져 있으면 종료
if [ -z $CURRENT_PID ]; then
  echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG
else
  echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG
  kill -15 $CURRENT_PID
fi

appspec.yml

version: 0.0
os: linux

files:
  - source:  /
    destination: /home/ec2-user/app
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  BeforeInstall:
    - location: scripts/init-dir.sh
      timeout: 30
      runas: ec2-user
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas: ec2-user

BackEnd CI/CD 파이프라인 시연

  • 스크립트도 다썼으니 잘 배포 되는지 확인해보는게 좋지 않을까? 시연해보자

0. 변경 사항 반영 전 사이트 → “bonbon project ver_1.2.0”


1. 변경 내용 작업 후 Github에 Commit & push → main branch로 PR & Merge

  • “bonbon project ver_1.2.2”로 변경한 후 Commit & push

2. main 브랜치 업데이트 시 webhook 발생 → git action workflows 실행


3. Git Action에서 Build & Deploy 완료, S3에 zip 파일 업로드


4. CodeDeploy에서 배포 시작

  • AutoScaling Group + Launch Template에 의해 새로운 EC2 인스턴스 2개 생성 → 대체 인스턴스에 .jar 파일 배포
  • AutoScaling Group 을 통해 배포된 인스턴스들에게 트래픽을 분산시킴
  • 대체 인스턴스에 배포 후 기존에 있던 원본 인스턴스(2개) traffic block 됨

5. 원본 인스턴스 종료 및 배포 종료

  • 새로 생성된 인스턴스들 상태 확인 → healthy
  • 기존의 원본 인스턴스 2개는 삭제된 것을 확인

6. Swagger에서 변경 사항 확인

  • “bonbon project ver_1.2.0”“bonbon project ver_1.2.2”
    로 바뀐 것을 확인할 수 있다.

BackEnd도 끝!

profile
백엔드 개발자 나무입니다

0개의 댓글