CI/CD 1 : GithubActions
Apply Tracker 프로젝트를 배포 자동화(CI/CD)해보고자 했고,
그 결과 선택한 도구가 GitHub Actions였다.
이 CI/CD시리즈에서는 전체 배포 인프라를 구축한 흐름을 소개하면서,
1편에서는 그 시작인 GitHub Actions 기반 자동화 배포 환경 구성 이유와 구현 과정을 정리해보려고 한다.
왜 자동 배포?
혼자서 또는 소규모 팀으로 프로젝트를 진행하다 보면
"코드 수정 → 테스트 → 서버 접속 → 배포 스크립트 실행"이라는
반복적인 수작업이 생각보다 시간을 많이 잡아먹는다.
무엇보다 사람 손이 들어가면 실수 확률도 높아지고,
여러 개발자가 동시에 작업하는 경우, 빌드 실패 / 파일 누락 / 테스트 누락 같은 문제가 쉽게 발생한다.
이런 반복적이고 단순하지만 중요한 배포 과정을 자동화하려면
CI/CD 환경을 제대로 구성하는 게 핵심이다.
CI/CD 란?
- CI : 소스코드 변경 시 자동으로 빌드 → 테스트 → 병합하는 프로세스를 말한다.
- CD: 테스트를 통과한 코드를 자동으로 운영 서버에 배포하는 프로세스를 말한다.
CI를 도입하면,
여러개발자의 코드가 하나의 브랜치에 자주 통합되고 자동 테스트로 코드 품질을 유지할 수 있다.
CD를 도입하면,
코드가 테스트에 통과하면 즉시 운영 환경에 배포되므로 배포 누락을 방지해주고
즉각적인 사용자 피드백을 수집 할 수 있다.
다양한 CI/CD 도구 비교
도구 | 특징 | 장점 | 단점 |
GitHub Actions | GitHub에 내장된 CI/CD 도구 | 설정이 간단, 무료 플랜, 깔끔한 UI | 서버 접근 보안 관리 주의 |
Jenkins | 가장 오래된 CI/CD 툴 | 커스터마이징 강력 | 서버 직접 구축 필요, 복잡함 |
GitLab CI | GitLab에 내장된 CI/CD 도구 | GitLab과 통합 우수 | GitHub 사용자에게는 번거로움 |
CircleCI / TravisCI | 외부 CI 서비스 | 다양한 언어와 연동, 유료 플랜 강력 | 사용량 초과 시 과금 우려 |
Github Actions를 선택한 이유
우선 Github Actions는 Github와 완벽하게 통합되어 따로 연동할 필요가 없어 처음 시도하기에 허들이 낮았다.
Github 서버에서 실행하기에 빌드 서버가 따로 필요가 없다는 점과
git pull 기반으로 변경된 파일만 받아오기에 배포 속도가 빠르다는 점
그리고 소규모 프로젝트에 적합한 무료 플랜을 제공하기에 Github Actions를 선택하게 되었다.
Github Actions는 push, pull_request, release 등의 이벤트를 트리거로
테스트, 빌드, 배포 등의 작업을 자동으로 처리해주는 워크플로우 자동화 플랫폼이다.
GitHub Acitons - deploy.yml
우선 .github 폴더를 생성하고, 그 안에 workflows 폴더를 생성한 다음 그 안에 deploy.yml 파일을 생성한다.
name: Resume Fullstack CI/CD
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 📦 Checkout Repository
uses: actions/checkout@v3
- name: Create application.yml from Secret
run: |
echo "${{ secrets.APPLICATION_YML }}" | base64 --decode > application.yml
- name: Upload application.yml to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: "application.yml"
target: "/home/ubuntu/application.yml"
#################################
# 1. Backend Build (Spring Boot)
#################################
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: ⚙️ Build Spring Boot Project
working-directory: backend
run: |
chmod +x gradlew
./gradlew clean build -x test
mkdir -p ../deploy
cp $(ls -t build/libs/*SNAPSHOT.jar | grep -v plain | head -n 1) ../app.jar
- name: 📂 Ensure upload directory exists
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
mkdir -p /home/ubuntu/server/web
- name: Upload JAR to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: "app.jar"
target: "/home/ubuntu/server/web/"
- name: Restart Spring Boot App
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
sudo fuser -k -n tcp 8080 || true
mkdir -p /home/ubuntu/server/web
echo "${{ secrets.APPLICATION_YML }}" | base64 --decode > /home/ubuntu/server/web/application.yml
cd /home/ubuntu/server/web
nohup java -jar app.jar \
--spring.config.location=file:/home/ubuntu/server/web/application.yml \
--spring.profiles.active=prod > output.log 2>&1 &
echo "Spring Boot started"
sleep 5
- name: 📂 확인 (디버깅용)
run: |
ls -al
file app.jar
#################################
# 2. Frontend Build (React + Vite)
#################################
- name: 🌍 Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: 📦 Install Dependencies
run: npm install
working-directory: frontend
- name: 🔧 Build React App (Vite)
run: |
npm run build
ls -al ./dist
working-directory: frontend
#################################
# 3. Upload to EC2
#################################
- name: 📤 Upload React Build to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: "frontend/dist/**"
target: "/home/ubuntu/frontend-build"
strip_components: 2 # dist 내부 경로 유지하지 않도록
- name: 🔁 Deploy to Nginx Folder
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
echo "✅ Before move:"
ls -al /home/ubuntu/frontend-build
sudo mkdir -p /var/www/html
sudo rm -rf /var/www/html/*
sudo cp -r /home/ubuntu/frontend-build/* /var/www/html/
echo "✅ After move:"
ls -al /var/www/html
sudo systemctl restart nginx
sudo rm -rf /home/ubuntu/frontend-build
Checkout Repository
기능: GitHub 레포지토리의 코드를 Actions 서버로 내려받는다.
- 가장 첫 번째 step에서 반드시 실행해야 이후 코드 작업이 가능하다.
- 특정 브랜치나 디렉토리만 받고 싶을 때는 with: 옵션 추가 가능하다.
Upload application.yml to EC2
기능: 위에서 생성한 application.yml을 EC2로 업로드한다.
- EC2의 /home/ubuntu/ 경로에 올려서 백엔드 jar 실행 시 참조한다.
- scp-action 사용 시 target 경로 정확히 지정해야 덮어씌움을 방지할 수 있다.
Build Spring Boot Project
기능: 백엔드(Spring Boot) 프로젝트를 빌드한다.
- -x test: 테스트 생략 (필요 시 제거)
- 최신 jar 파일만 추출해서 app.jar로 저장했다.
- build/libs/에 여러 jar 파일이 생성될 수 있으므로 plain.jar 제외 로직을 추가했다.
Ensure upload directory exists
기능: EC2 내부에 jar 파일을 업로드할 디렉토리를 미리 생성한다. (/home/ubuntu/server/web)
- 없는 디렉토리에 scp하면 에러 발생하므로 사전에 생성했다.
Upload JAR to EC2
기능: 빌드된 app.jar을 EC2 서버로 전송한다.
Restart Spring Boot App
기능: 기존 8080 포트를 종료하고 application.yml 재적용 한다음 Spring Boot 앱을 실행한다.
- fuser -k로 기존 프로세스 종료한다. (포트 충돌 방지)
- nohup으로 백그라운드 실행 + 로그 파일을 확인한다. (output.log)
Frontend Build (Vite + React)
기능: Node.js 설치하고 npm install로 의존성 설치하고 npm run build로 Vite 앱 번들링한다.
- working-directory: frontend 설정 꼭 필요
- dist 폴더가 생성되는 위치 주의
Upload React Build to EC2
기능: dist/ 폴더의 번들링 결과물을 EC2의 /home/ubuntu/frontend-build로 전송한다.
- strip_components: 2 없으면 dist 폴더 하위 경로가 꼬일 수 있다.
Deploy to Nginx Folder
기능: React 정적 리소스를 var/www/html로 이동해 이전 파일 제거 후 최신 빌드 복사하고 Nginx 재시작한다.
- 배포 실패 시 /var/www/html이 비는 경우 발생 가능 → 백업 전략 필요하다.
Github Actions CI/CD 핵심 포인트
- 민감한 정보는 Github Secrets에 보관: 절대 .yml 파일에 직접 적성하지 않는다.
- AWS EC2 접속용 .pem키, 서버 IP, application.yml, EC2 유저명
- Application.yml 파일은 base 64로 인코딩 후 배포시 Github Secrets에서 가져와 decoding한다.
- 디버깅 로그는 nohup + output.log로 확인할 수 있다.
마무리
이처럼 GitHub Actions를 활용하면
백엔드 빌드 → EC2 배포 → React 프론트엔드 빌드 → Nginx 반영까지
풀스택 애플리케이션의 배포 과정을 완전 자동화할 수 있다.