[SpringBoot] GitHub Actions + S3 + CodeDeploy를 이용한 EC2 CI/CD 구축

이재윤·2025년 7월 10일

SpringBoot와 Devops

목록 보기
5/6

🚀 GitHub Actions + S3 + CodeDeploy를 이용한 EC2 CI/CD 구축


💡 CI/CD란?

CI/CD는 개발자가 작성한 코드를 자동으로 빌드, 테스트, 배포하는 파이프라인을 의미합니다.

  • CI (Continuous Integration, 지속적 통합)
    여러 개발자의 코드 변경 사항을 자주 메인 브랜치에 통합하고, 자동 빌드 + 테스트를 통해 문제를 빠르게 발견합니다.

  • CD (Continuous Deployment, 지속적 배포)
    빌드와 테스트를 통과한 코드를 운영 서버에 자동 배포하는 과정입니다.
    수동 배포 없이 안정적이고 빠른 릴리스를 가능하게 해줍니다.

CI/CD를 도입하면?

  • 수동 배포 실수 방지
  • 빠르고 안정적인 개발 사이클 구축
  • 팀 생산성과 코드 품질 향상

🧩 전체 아키텍처 구성

  • GitHub에 push하면
  • GitHub Actions가 빌드 후 zip 파일을 S3에 업로드
  • CodeDeploy가 S3에서 zip 파일을 받아 EC2로 배포
  • EC2에서는 appspec.yml과 스크립트에 따라 자동 실행

🛠️ Code Deploy 설정

1. AWS EC2에 Tag 설정

EC2 인스턴스 설정에서 태그 관리를 누르고 태그를 확인

2. AWS EC2에 IAM 설정

역할 생성 -> EC2 선택 -> AmazonS3FullAccess 권한 추가 -> EC2 인스턴스에서 IAM 역할 수정

3. 서버에 CodeDeploy Agent 설치

#패키지 설치
sudo apt update -y
sudo apt install ruby wget -y

#CodeDeploy 에이전트 설치
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

#에이전트 정상 동작 확인
sudo service codedeploy-agent status

4. AWS S3 생성

S3 버킷 생성(버킷 이름, AWS 리전, ACL 비활성화, 퍼블릭 엑세스 차단, 버킷 버전 관리 비활성화, 버킷 키 비활성화)

5. AWS CodeDeploy 전용 IAM 역할 생성

역할 생성 -> CodeDeploy 선택 -> AWSCodeDeployRole 권한 추가

6. AWS CodeDeploy Application 생성 및 배포 설정

CodeDeploy 애플리케이션 생성 -> 배포 그룹 생성 -> 서비스 역할에 CodeDeploy IAM 추가 -> 환경구성 EC2 인스턴스 -> 태그 그룹 -> 에이전트 한 번만 설치 -> 배포 설정 -> 로드 밸런서 비활성화

7. Github Actions의 사용자 권한 추가

IAM 사용자 생성 -> AWSCodeDeployFullAccess, AmazonS3FullAccess 권한 추가 -> 보안 자격 증명 탭에서 엑세스 키 만들기 -> 기타 -> Github Repository -> Settings -> Secrets and variables -> Actions -> New repository secret에 엑세스 키, 비밀 키 만들기

8. App Spec 작성

version: 0.0
os: linux

#배포 파일 설정
files:
  - source: /
    destination: /home/ubuntu/project
    overwrite: yes

#이미 있을 경우 덮어쓰기
file_exists_behavior: OVERWRITE

#files 섹션에서 복사한 파일에 대한 권한 설정
permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

#배포 이후에 실행할 일련의 라이프사이클
hooks:
  BeforeInstall:
    - location: scripts/cleanup.sh
      timeout: 60
      runas: ubuntu
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ubuntu
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas: ubuntu

9. 배포 스크립트(cleanup.sh, start.sh, stop.sh) 작성 & build.gradle 파일 수정

1) scripts/cleanup.sh

#!/usr/bin/env bash

# 프로젝트 경로
PROJECT_ROOT="/home/ubuntu/ptufestival"
OLD_DIR="$PROJECT_ROOT/old"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)

# old 디렉토리 없으면 생성
mkdir -p "$OLD_DIR"

# 기존 jar 백업
if [ -f "$PROJECT_ROOT/spring-webapp.jar" ]; then
  echo "$TIME_NOW > 기존 JAR 백업 시작" >> $DEPLOY_LOG
  mv "$PROJECT_ROOT/spring-webapp.jar" "$OLD_DIR/spring-webapp-$(date +%Y%m%d%H%M%S).jar"
  echo "$TIME_NOW > spring-webapp.jar 백업 완료" >> $DEPLOY_LOG
else
  echo "$TIME_NOW > 백업할 기존 JAR 없음" >> $DEPLOY_LOG
fi

# 오래된 로그 정리 (선택)
find "$PROJECT_ROOT" -name "*.log" -type f -mtime +14 -exec rm -f {} \;
echo "$TIME_NOW > 14일 초과 로그 파일 정리 완료" >> $DEPLOY_LOG

2) scripts/start.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ubuntu/project"
JAR_SOURCE=$(ls $PROJECT_ROOT/build/libs/*.jar | tail -n 1)
JAR_TARGET="$PROJECT_ROOT/spring-webapp.jar"

APP_LOG="$PROJECT_ROOT/app.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
PORT=443

echo "$TIME_NOW > 새 애플리케이션 배포 시작" >> $DEPLOY_LOG

if [ ! -f "$JAR_SOURCE" ]; then
  echo "$TIME_NOW > JAR 파일이 존재하지 않아 복사 실패!" >> $DEPLOY_LOG
  exit 1
fi

echo "$TIME_NOW > JAR 복사: $JAR_SOURCE -> $JAR_TARGET" >> $DEPLOY_LOG
cp "$JAR_SOURCE" "$JAR_TARGET"

chmod +x "$JAR_TARGET"
echo "$TIME_NOW > 실행 권한 부여 및 애플리케이션 실행" >> $DEPLOY_LOG

# 백그라운드 실행
sudo nohup java -Duser.timezone=Asia/Seoul -jar "$JAR_TARGET" > "$APP_LOG" 2> "$ERROR_LOG" &

sleep 5

# 포트 점유 상태 확인
if sudo lsof -i :$PORT -sTCP:LISTEN > /dev/null; then
  echo "$TIME_NOW > 포트 $PORT 정상적으로 점유됨. 실행 성공 추정" >> $DEPLOY_LOG
  sudo lsof -i :$PORT -sTCP:LISTEN >> $DEPLOY_LOG
else
  echo "$TIME_NOW > 포트 $PORT 점유 안됨. 실행 실패 가능성 있음" >> $DEPLOY_LOG
  tail -n 20 "$ERROR_LOG" >> $DEPLOY_LOG
fi

3) scripts/stop.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ubuntu/project"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
PORT=443

echo "$TIME_NOW > 실행 중인 애플리케이션 종료 시도" >> $DEPLOY_LOG

# 443 포트 LISTEN 중인 PID 탐색
CURRENT_PID=$(sudo lsof -i :$PORT -sTCP:LISTEN -t)

if [ -z "$CURRENT_PID" ]; then
  echo "$TIME_NOW > 포트 $PORT LISTEN 중인 프로세스 없음" >> $DEPLOY_LOG
else
  echo "$TIME_NOW > 포트 $PORT LISTEN 중인 PID $CURRENT_PID 종료 시도" >> $DEPLOY_LOG
  sudo kill -15 "$CURRENT_PID"
  sleep 3

  if ps -p "$CURRENT_PID" > /dev/null; then
    echo "$TIME_NOW > 정상 종료 실패, 강제 종료 시도" >> $DEPLOY_LOG
    sudo kill -9 "$CURRENT_PID"
  fi
fi

10. Github Actions Workflow 작성
project/.github/workflows/deploy.yml

name: Deploy to Amazon EC2

on:
  push:
    branches:
      - main

env:
  AWS_REGION: ap-northeast-2
  PROJECT_NAME: project
  S3_BUCKET_NAME: {secrets.S3_BUCKET_NAME}
  CODE_DEPLOY_APPLICATION_NAME: your_code_deploy_name
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: your_code_group_name

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Build with Gradle
        run: ./gradlew clean build --stacktrace --info

      - 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_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - 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 .

      - name: Deploy to AWS EC2 from S3
        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=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip

🚨 env와 secrets 만드는 부분은 본인에 맞게 수정하셔야 됩니다.

11. Github에서 push로 배포 확인

git push 후 Actions에서 잘 되는지 확인하기


🗂️ 프로젝트 구조

project/
├── .github/
│   └── workflows/
│       └── deploy.yml       # GitHub Actions 워크플로우
├── appspec.yml              # CodeDeploy 배포 설정
├── scripts/
│   ├── start.sh             # 애플리케이션 실행 스크립트
│   ├── stop.sh              # 애플리케이션 종료 스크립트
│   └── cleanup.sh           # 이전 버전 정리
├── build/libs/
│   └── spring-webapp.jar           # 빌드 산출물
└── ...

✅ 배포 확인 체크리스트

  • EC2 인스턴스에 CodeDeploy Agent가 실행 중인가?
  • IAM 권한 문제는 없는가?
  • GitHub Secrets는 올바르게 설정했는가?
  • Shell script에 실행 권한(chmod +x) 부여했는가?

🏁 마무리

CI/CD의 장점은 개발자에게 신속한 배포, 운영자에게 안정성, 팀 전체에게는 생산성 향상을 가져다 줄 수 있습니다.





아직 학생이라 부족한 점이 있을 수 있습니다. 댓글과 피드백은 언제든지 환영입니다!

profile
부족한 점이 많습니다. 피드백은 환영입니다!

0개의 댓글