본문 바로가기

DevOps/CICD

[Springboot + react] CICD 구축 _ github action_deploy.sh (프론트+백 단일 서버)

반응형

목표/상황

  • 스택: React(프론트) + Spring Boot(백엔드)
  • 요구
    • SSL은 당장 안 씀
    • 서버는 8081 포트로 띄움
    • 프론트/백 각각 별도 GitHub 레포 관리
    • 지금은 프론트 빌드 결과를 스프링에서 정적 리소스로 서빙
  • 배포 형태(최종 결정)
    • 단일 디렉토리 방식: /home/company_name/project_name 아래에 다 모음
      • 백엔드 JAR: /home/company_name/project_name/project_name.jar
      • 로그: /home/company_name/project_name/project_name.log
      • 프론트 빌드: /home/company_name/project_name/frontend/
    • 프론트는 rsync로 해당 폴더에 업로드 → Spring이 서빙
    • 백엔드는 scp로 JAR 업로드 + deploy.sh 실행해서 재시작

 

서버 준비 (1회)

# 배포 계정: my_nickname (sudo 포함)
adduser my_nickname
usermod -aG sudo my_nickname

# SSH 키 등록 (로컬에서 생성한 id_rsa.pub 등록)
# 권한 필수: ~/.ssh = 700, authorized_keys = 600
mkdir -p /home/my_nickname/.ssh
echo "<로컬의 id_rsa.pub 한 줄>" >> /home/my_nickname/.ssh/authorized_keys
chmod 700 /home/my_nickname/.ssh
chmod 600 /home/my_nickname/.ssh/authorized_keys
chown -R my_nickname:my_nickname /home/my_nickname/.ssh

# root 비번 로그인 유도 막는 기본키 제거(해당되는 경우)
# (authorized_keys에 no-port-forwarding… 같은 라인들 지움)

# PasswordAuthentication 잠시 yes로 확인 후, 키 접속 OK되면 no로 변경
cat >/etc/ssh/sshd_config.d/99-pubkey.conf <<'EOF'
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
EOF
systemctl reload sshd

# 배포 디렉토리
sudo mkdir -p /home/company_name/project_name/frontend
sudo chown -R my_nickname:my_nickname /home/company_name/project_name

최종적으로 my_nickname로 비번 없이 SSH 접속 되야 함.

 

Spring Boot 설정

application.properties

# 프론트 정적 파일을 여기서 서빙
spring.web.resources.static-locations=file:/home/company_name/project_name/frontend/,classpath:/static/

(운영 중엔 캐시 이슈가 있으면 index.html만 no-cache로 따로 조정 권장)

 

deploy.sh (백엔드 재시작 스크립트)

/home/company_name/project_name/deploy.sh

#!/usr/bin/env bash
set -euo pipefail

APP_DIR="/home/company_name/project_name"
JAR="$APP_DIR/project_name.jar"
LOG="$APP_DIR/project_name.log"
PID_FILE="$APP_DIR/project_name.pid"

stop_if_running() {
  if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
    echo "Stopping PID $(cat "$PID_FILE")"
    kill "$(cat "$PID_FILE")" || true
    sleep 3
  fi
  pkill -f "java .*project_name.jar" || true
}

rotate_log() {
  [[ -f "$LOG" ]] && mv "$LOG" "$LOG.$(date +%Y%m%d%H%M%S)"
}

start_app() {
  nohup java -jar "$JAR" \\
    --spring.profiles.active=prod \\
    --server.port=8081 \\
    --spring.web.resources.static-locations=file:/home/company_name/project_name/frontend/,classpath:/static/ \\
    >> "$LOG" 2>&1 &
  echo $! > "$PID_FILE"
  echo "Started. PID=$(cat "$PID_FILE")"
}

stop_if_running
rotate_log
start_app

chmod +x /home/company_name/project_name/deploy.sh

GitHub Secrets (각 레포에 동일하게 2개)

  • SSH_HOST = 배포 서버 주소 
  • SSH_KEY = 로컬 ~/.ssh/id_rsa 전체 내용 (BEGIN/END 포함)

프론트 레포, 백엔드 레포 각각 Settings → Secrets and variables → Actions에서 등록.

 

프론트 CI/CD (menu-picker-front)

.github/workflows/deploy-frontend.yml

name: Deploy Frontend (rsync to server)
on:
  push:
    branches: ["main"]
  workflow_dispatch: {}

concurrency:
  group: frontend-deploy
  cancel-in-progress: true

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "18"
          cache: "npm"   # package-lock.json 없으면 캐시만 미사용(동작엔 문제 없음)

      - name: Install & Build
        run: |
          # npm ci 쓰려면 레포에 package-lock.json이 있어야 함
          # 없다면 아래 줄을 npm install 로 변경해도 됨
          npm ci
          CI=false npm run build    # CRA면 CI=false, Vite/Next면 npm run build

      - name: Deploy build/ to server via rsync
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avzr --delete
          path: build/                        # 산출물이 dist면 dist/로 변경
          remote_path: /home/company_name/project_name/frontend/
          remote_host: ${{ secrets.SSH_HOST }}
          remote_user: my_nickname
          remote_key: ${{ secrets.SSH_KEY }}

 

액션이 “안 뜬다” 싶으면

  • 파일 위치: .github/workflows/deploy-frontend.yml
  • 브랜치: main에 커밋/머지 맞는지
  • workflow_dispatch로 수동 실행해보기
  • 레포 Settings → Actions 권한 확인

 

백엔드 CI/CD (menu-picker-backend)

.github/workflows/deploy-backend.yml

name: Deploy Backend (scp jar to server)

on:
  push:
    branches: ["main"]

concurrency:
  group: backend-deploy
  cancel-in-progress: true

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
          cache: gradle

      - name: Make gradlew executable
        run: chmod +x ./gradlew

      - name: Build with Gradle
        run: ./gradlew clean build -x test

      - name: Upload JAR
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SSH_HOST }}
          username: my_nickname
          key: ${{ secrets.SSH_KEY }}
          source: "build/libs/*.jar"
          target: "/home/company_name/project_name/"

      - name: Rename & restart (deploy.sh)
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: my_nickname
          key: ${{ secrets.SSH_KEY }}
          script: |
            set -e
            cd /home/company_name/project_name
            mv "$(ls -t *.jar | head -n1)" project_name.jar
            ./deploy.sh
            sleep 3
            curl -sf <http://127.0.0.1:8081/> || true

 

Gradle Wrapper 이슈(내가 겪은 포인트)

  • CI에서 gradle-wrapper.jar 를 못 찾을 때가 있었음→ 예외를 .gitignore 맨 아래에 추가 + 강제 add
    git add -f gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties gradlew gradlew.bat
    git update-index --chmod=+x gradlew
    git commit -m "fix: include Gradle wrapper files"
    git push
    
  • # --- Allow Gradle Wrapper (must be after *.jar rule) --- !gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.properties !gradlew !gradlew.bat
  • → .gitignore에 *.jar가 있어서 래퍼 JAR이 무시됨

 

배포 후 확인 루틴

# 백엔드 프로세스
ps -ef | grep project_name.jar

# 포트 확인
ss -tulnp | grep 8081

# 백엔드 로그
tail -f /home/company_name/project_name/project_name.log

# 서버에서 직접 index.html 일부 확인
curl -s <http://127.0.0.1:8081/> | head

브라우저에서 새 UI가 안 보이면 강력 새로고침(Cmd/ Ctrl + Shift + R) 또는 /?v=now로 캐시 우회.

 

트러블슈팅 

  • SSH가 계속 비밀번호를 물음
    • authorized_keys에 공개키가 정확히 한 줄인지
    • 권한: ~/.ssh = 700, authorized_keys = 600
    • /etc/ssh/sshd_config.d/99-pubkey.conf 로 PubkeyAuthentication yes 강제
    • passwd -d <user>로 비밀번호 로그인 제거 (키 로그인만 허용)
  • 프론트가 안 반영됨
    • 실제 파일은 /home/company_name/project_name/frontend/로 잘 올라왔는지 ls로 확인
    • Spring의 정적 경로가 해당 폴더인지(실행 옵션으로 강제)
    • 브라우저 캐시
  • Actions가 안 뜸
    • 워크플로우 경로/파일명 오타
    • 브랜치 트리거 불일치
    • 레포 Settings에서 Actions 제한
    • 일단 workflow_dispatch 추가해서 수동 트리거
  • npm 캐시 경고: package-lock.json 없으면 캐시만 미사용 (실행은 됨).
  • 필요하면 npm install로 변경 or package-lock.json 커밋.

 

(선택) 보안/운영 개선 TODO

  • PermitRootLogin no, UFW로 22/8081만 허용
  • systemd 서비스로 전환(재부팅 자동 기동/모니터링 편함)
  • Ubuntu 22.04로 업그레이드 검토
  • 프론트 정적 자원은 해시 파일명(Long-term cache) 전략 정착

 

최종 흐름 (한 줄 요약)

  • 프론트: main에 push → Actions가 build → /home/company_name/project_name/frontend/에 rsync → Spring이 즉시 서빙
  • 백엔드: main에 push → Actions가 Gradle build → 최신 JAR scp → deploy.sh로 8081 재기동

 

이제 main에 올리면 자동으로 서버 반영된다 😎

 

반응형

'DevOps > CICD' 카테고리의 다른 글

개발서버 배포 자동화 deploy.sh (쉘 스크립트 작성)  (0) 2024.05.05