반응형
목표/상황
- 스택: 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 실행해서 재시작
- 단일 디렉토리 방식: /home/company_name/project_name 아래에 다 모음
서버 준비 (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 |
|---|