- 05-deployment.md: Jenkinsfile 코드블록 전체 현행화 - React/API/MNG: slackSend + tokenCredentialId 추가 - API/MNG: mkdir-p bootstrap/cache, storage/framework 추가 - MNG: npm install --production=false → --prefer-offline - 수동배포 섹션: mkdir-p 추가, 단계 번호 재정렬 - 빌드 실패 트러블슈팅: Laravel 디렉토리 누락 항목 추가 - 07-monitoring.md: Contact Point TODO → 실제 설정 완료 내용 반영 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
36 KiB
5. 배포 가이드
파이프라인 개요
전체 흐름
개발자 push -> 개발서버 Gitea -> post-receive hook -> CI/CD Gitea push
-> Webhook -> Jenkins -> 빌드/배포
파이프라인 구성
| 저장소 | 파이프라인 | 트리거 브랜치 | 배포 대상 |
|---|---|---|---|
| sam-react-prod | React 빌드+배포 | develop, main | 개발 / Stage→승인→운영 |
| sam-api | Laravel API 배포 | main | Stage→승인→운영 |
| sam-manage | Laravel Admin 배포 | main | 운영 (직접) |
| sam-sales | 레거시 PHP 배포 | main | 운영 (직접) |
2-Branch 전략 (develop + main)
stage 브랜치 없음. main 브랜치 push 시 Stage 자동 배포 → Jenkins 승인 → Production 배포.
| 브랜치 | react | api | mng | sales |
|---|---|---|---|---|
| develop | Jenkins 빌드 → 개발서버 | 기존 post-update hook | 기존 post-update hook | 기존 post-update hook |
| main | Stage 배포 → 승인 → Production 배포 | Stage 배포 → 승인 → Production 배포 | Production 직접 배포 | Production 직접 배포 |
main 브랜치 배포 흐름 (react/api):
- 개발자가 develop → main 머지 후 push
- post-receive hook → CI/CD Gitea 자동 push
- Jenkins 빌드 → Stage 자동 배포
- Jenkins UI에서 승인 클릭 → Production 배포 (24시간 타임아웃)
main 브랜치 배포 흐름 (mng/sales):
- 개발자가 main push → hook → CI/CD Gitea → Jenkins → Production 직접 배포
Git 동기화 전략
방침: 개발서버 Gitea(origin) 유지 + CI/CD Gitea에 선택적 브랜치 push (post-receive hook)
Gitea Push Mirror는 전체 브랜치를 미러링하므로 사용하지 않음. 대신 개발서버 Gitea의 post-receive hook으로 필요한 브랜치만 CI/CD Gitea에 push.
개발자 로컬
│ git push origin (develop / main)
▼
개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자의 origin
│
├─ develop push 시
│ ├─ api/mng/sales: 기존 post-update hook (개발서버 pull) ← 현행 유지
│ └─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 개발서버 배포
│
└─ main push 시
├─ react: hook → CI/CD Gitea → Jenkins 빌드 → Stage 배포 → 승인 → Production 배포
├─ api: hook → CI/CD Gitea → Jenkins → Stage 배포 → 승인 → Production 배포
├─ mng: hook → CI/CD Gitea → Jenkins → Production 직접 배포
└─ sales: hook → CI/CD Gitea → Jenkins → Production 직접 배포
브랜치별 배포 정책 상세
| 브랜치 | 저장소 | CI/CD Gitea 동기화 | Jenkins 배포 | 배포 대상 |
|---|---|---|---|---|
| main | react | 자동 (hook) | 빌드 → Stage → 승인 → 재빌드 → Production | Stage + Production |
| main | api | 자동 (hook) | rsync → Stage → 승인 → rsync → Production | Stage + Production |
| main | mng | 자동 (hook) | rsync + npm build → Production | Production |
| main | sales | 자동 (hook) | rsync → Production | Production |
| develop | react | 자동 (hook) | 빌드 → 개발서버 배포 | 개발서버 |
| develop | api/mng/sales | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) |
post-receive hook 동기화 요약
| 저장소 | hook 대상 브랜치 | 동작 |
|---|---|---|
| sam-react-prod | main, develop | CI/CD Gitea에 push |
| sam-api | main | CI/CD Gitea에 push |
| sam-manage | main | CI/CD Gitea에 push |
| sam-sales | main | CI/CD Gitea에 push |
| sam-landing | main | CI/CD Gitea에 push |
hook 스크립트 경로: /data/GIT/samproject/<repo>.git/hooks/post-receive.d/push-to-cicd
토큰 환경변수: /data/GIT/.cicd-env (chmod 600, owner: git)
Webhook 설정 (CI/CD Gitea → Jenkins)
각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI):
Repository Settings → Webhooks → Add Webhook (Gitea)
- URL: https://ci.sam.it.kr/gitea-webhook/post
- Content Type: application/json
- Secret: <webhook_secret>
- Events: Push events
배포 흐름도
개발자 로컬
│ git push origin (develop / main)
▼
┌──────────────────────────────────────────────────────────────┐
│ 개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자 origin │
│ │
│ post-receive hooks: │
│ │
│ ┌─ develop push ────────────────────────────────────────┐ │
│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │
│ │ → 빌드 결과 rsync → 개발서버 배포 │ │
│ │ api → 기존 post-update hook (pull + migrate) │ │
│ │ mng → 기존 post-update hook (pull + build) │ │
│ │ sales → 기존 post-update hook (pull) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌─ main push (모든 저장소 자동) ────────────────────────┐ │
│ │ react → hook: CI/CD Gitea push ──→ Jenkins │ │
│ │ → Stage 빌드+배포 → 승인 → Production 재빌드 │ │
│ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │
│ │ → Stage rsync+배포 → 승인 → Production 배포 │ │
│ │ mng → hook: CI/CD Gitea push ──→ Jenkins │ │
│ │ → Production rsync + build │ │
│ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │
│ │ → Production rsync │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
┌─ Jenkins 승인 흐름 (react/api main) ─────────────────────────┐
│ │
│ Jenkins 빌드 시작 │
│ │ │
│ ├─ Stage 자동 배포 (react: .env.stage 빌드) │
│ │ │
│ ├─ ⏸️ 승인 대기 (24시간 타임아웃) │
│ │ https://ci.sam.it.kr 에서 "운영 배포 진행" 클릭 │
│ │ │
│ ├─ Production 배포 (react: .env.main 재빌드) │
│ │ │
│ └─ 완료 │
│ │
└───────────────────────────────────────────────────────────────┘
환경별 배포 비교
| 항목 | Production (main→승인) | Stage (main→자동) | 개발 (develop) |
|---|---|---|---|
| 트리거 | main push → Jenkins 승인 | main push → 자동 | react만 자동 (hook), 나머지 기존 hook |
| react 전략 | CI/CD 빌드(.env.main) → rsync | CI/CD 빌드(.env.stage) → rsync | CI/CD 빌드(.env.develop) → rsync |
| api 전략 | rsync + Release 심링크 | rsync + Release 심링크 | 기존 post-update (pull) |
| mng 전략 | rsync + npm build + Release 심링크 | - | 기존 post-update (pull + build) |
| 롤백 | 이전 릴리즈 심링크 | 이전 릴리즈 심링크 | git revert |
| 릴리즈 보관 | 최근 5개 | 최근 3개 | - |
React (Next.js) 배포
자동 배포 흐름
CI/CD Gitea push -> Webhook -> Jenkins
-> npm install -> npm run build -> rsync -> PM2 reload
브랜치별 배포 대상:
| 브랜치 | 배포 단계 | 대상 서버 | 대상 경로 | PM2 이름 |
|---|---|---|---|---|
| develop | 개발서버 | 114.203.209.83 | /home/webservice/react/ | sam-react |
| main | Stage (자동) | 211.117.60.189 | /home/webservice/react-stage/releases/ | sam-front-stage |
| main | Production (승인 후) | 211.117.60.189 | /home/webservice/react/releases/ | sam-front |
환경변수 파일 (CI/CD 서버): /var/lib/jenkins/env-files/react/
| 파일 | API URL | Frontend URL |
|---|---|---|
| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com |
| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr |
| .env.main | https://api.sam.it.kr | https://sam.it.kr |
rsync 주의: trailing slash 사용 금지: .next (O), .next/ (X)
릴리즈 보관: 운영 5개, Stage 3개
Jenkinsfile (react/Jenkinsfile)
pipeline {
agent any
environment {
DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
}
stages {
stage('Checkout') {
steps {
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
checkout scm
}
}
stage('Prepare Env') {
steps {
script {
if (env.BRANCH_NAME == 'main') {
// main: Stage 빌드 먼저 (승인 후 Production 재빌드)
sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.local"
} else {
def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}"
sh "cp ${envFile} .env.local"
}
}
}
}
stage('Install') {
steps { sh 'npm install --prefer-offline' }
}
stage('Build') {
steps { sh 'npm run build' }
}
// ── develop → 개발서버 배포 ──
stage('Deploy Development') {
when { branch 'develop' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
rsync -az --delete \
--exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \
.next package.json next.config.ts public node_modules \
${DEPLOY_USER}@114.203.209.83:/home/webservice/react/
scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local
ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react'
"""
}
}
}
// ── main → 운영서버 Stage 배포 ──
stage('Deploy Stage') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}'
rsync -az --delete \
.next package.json next.config.ts public node_modules \
${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/
scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local
ssh ${DEPLOY_USER}@211.117.60.189 '
ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current &&
cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 &&
cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
'
"""
}
}
}
// ── 운영 배포 승인 ──
stage('Production Approval') {
when { branch 'main' }
steps {
timeout(time: 24, unit: 'HOURS') {
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr',
ok: '운영 배포 진행'
}
}
}
// ── main → Production 재빌드 (운영 환경변수) ──
stage('Rebuild for Production') {
when { branch 'main' }
steps {
sh "cp /var/lib/jenkins/env-files/react/.env.main .env.local"
sh 'npm run build'
}
}
// ── main → 운영서버 Production 배포 ──
stage('Deploy Production') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}'
rsync -az --delete \
.next package.json next.config.ts public node_modules \
${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/
scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local
ssh ${DEPLOY_USER}@211.117.60.189 '
ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current &&
cd /home/webservice && pm2 reload sam-front &&
cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
'
"""
}
}
}
}
post {
success {
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
}
failure {
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
}
}
}
참고: Next.js는
NEXT_PUBLIC_*환경변수가 빌드 시 바인딩되므로, Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다. main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드.
PM2 수동 재시작
ssh sam-prod
# 무중단 재시작 (cluster 모드)
pm2 reload sam-front
pm2 status
# 전체 재기동 필요한 경우
pm2 stop sam-front
cd /home/webservice && pm2 start ecosystem.config.js --only sam-front
pm2 save
API (Laravel) 배포
자동 배포 흐름
CI/CD Gitea push -> Webhook -> Jenkins
-> checkout -> rsync → Stage 배포 → 승인 → rsync → Production 배포
브랜치별 배포 대상:
| 브랜치 | 배포 단계 | 대상 서버 | 대상 경로 |
|---|---|---|---|
| main | Stage (자동) | 운영서버 | /home/webservice/api-stage/releases/ |
| main | Production (승인 후) | 운영서버 | /home/webservice/api/releases/ |
| develop | 개발서버 | 개발서버 | 기존 post-update hook |
Jenkinsfile (api/Jenkinsfile)
pipeline {
agent any
environment {
DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
}
stages {
stage('Checkout') {
steps {
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
message: "🚀 *api* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
checkout scm
}
}
// ── main → 운영서버 Stage 배포 ──
stage('Deploy Stage') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}'
rsync -az --delete \
--exclude='.git' --exclude='.env' \
--exclude='storage/app' --exclude='storage/logs' \
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api-stage/releases/${RELEASE_ID}/
ssh ${DEPLOY_USER}@211.117.60.189 '
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
ln -sfn /home/webservice/api-stage/shared/.env .env &&
ln -sfn /home/webservice/api-stage/shared/storage/app storage/app &&
composer install --no-dev --optimize-autoloader --no-interaction &&
php artisan config:cache &&
php artisan route:cache &&
php artisan view:cache &&
php artisan migrate --force &&
ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current &&
sudo systemctl reload php8.4-fpm &&
cd /home/webservice/api-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
'
"""
}
}
}
// ── 운영 배포 승인 ──
stage('Production Approval') {
when { branch 'main' }
steps {
timeout(time: 24, unit: 'HOURS') {
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr',
ok: '운영 배포 진행'
}
}
}
// ── main → 운영서버 Production 배포 ──
stage('Deploy Production') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}'
rsync -az --delete \
--exclude='.git' --exclude='.env' \
--exclude='storage/app' --exclude='storage/logs' \
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api/releases/${RELEASE_ID}/
ssh ${DEPLOY_USER}@211.117.60.189 '
cd /home/webservice/api/releases/${RELEASE_ID} &&
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
ln -sfn /home/webservice/api/shared/.env .env &&
ln -sfn /home/webservice/api/shared/storage/app storage/app &&
composer install --no-dev --optimize-autoloader --no-interaction &&
php artisan config:cache &&
php artisan route:cache &&
php artisan view:cache &&
php artisan migrate --force &&
ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current &&
sudo systemctl reload php8.4-fpm &&
sudo supervisorctl restart sam-queue-worker:* &&
cd /home/webservice/api/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
'
"""
}
}
}
// develop → Jenkins 관여 안함 (기존 post-update hook 유지)
}
post {
success {
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
}
failure {
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
script {
if (env.BRANCH_NAME == 'main') {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 '
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) &&
[ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
sudo systemctl reload php8.4-fpm
' || true
"""
}
}
}
}
}
}
참고: Laravel은 런타임 .env를 사용하므로 Stage/Production 별도 빌드가 필요 없다. 각 환경의 shared/.env가 심링크로 연결된다.
수동 배포 절차 (API Production)
참고: CI/CD Gitea는
REQUIRE_SIGNIN_VIEW = true설정이므로, 수동 git clone 시https://사용자:비밀번호@git.sam.it.kr/...형식 또는 CI/CD 서버에서 rsync로 전송하는 방식을 사용한다.
ssh sam-prod
# 1. 새 릴리즈 디렉토리 생성
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
cd /home/webservice/api/releases
git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git $RELEASE_ID
# 2. shared 심링크 연결
ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/$RELEASE_ID/storage
ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/$RELEASE_ID/.env
# 3. 필수 디렉토리 생성 (.gitignore에 의해 누락)
cd /home/webservice/api/releases/$RELEASE_ID
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs
# 4. 의존성 설치
composer install --no-dev --optimize-autoloader --no-interaction
# 5. 캐시 생성
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 6. 마이그레이션 (필요시)
php artisan migrate --force
# 7. 심링크 전환 (이 시점에 배포 적용)
ln -sfn /home/webservice/api/releases/$RELEASE_ID /home/webservice/api/current
# 8. 서비스 리로드
sudo systemctl reload php8.4-fpm
sudo supervisorctl restart sam-queue-worker:*
# 9. 오래된 릴리즈 정리 (최근 5개만 유지)
cd /home/webservice/api/releases
ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
수동 배포 절차 (API Stage)
ssh sam-prod
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
cd /home/webservice/api-stage/releases
git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git $RELEASE_ID
ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/$RELEASE_ID/storage
ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/$RELEASE_ID/.env
cd /home/webservice/api-stage/releases/$RELEASE_ID
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs
composer install --no-dev --optimize-autoloader --no-interaction
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
ln -sfn /home/webservice/api-stage/releases/$RELEASE_ID /home/webservice/api-stage/current
sudo systemctl reload php8.4-fpm
# 최근 3개만 유지
cd /home/webservice/api-stage/releases
ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
MNG (Laravel Admin) 배포
API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요.
Jenkinsfile (mng/Jenkinsfile)
pipeline {
agent any
environment {
DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
}
stages {
stage('Checkout') {
steps {
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
message: "🚀 *mng* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
checkout scm
}
}
// ── main → 운영서버 Production ──
stage('Deploy Production') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/mng/releases/${RELEASE_ID}'
rsync -az --delete \
--exclude='.git' --exclude='.env' \
--exclude='storage/app' --exclude='storage/logs' \
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
--exclude='node_modules' \
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/mng/releases/${RELEASE_ID}/
ssh ${DEPLOY_USER}@211.117.60.189 '
cd /home/webservice/mng/releases/${RELEASE_ID} &&
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
ln -sfn /home/webservice/mng/shared/.env .env &&
ln -sfn /home/webservice/mng/shared/storage/app storage/app &&
composer install --no-dev --optimize-autoloader --no-interaction &&
npm install --prefer-offline &&
npm run build &&
php artisan config:cache &&
php artisan route:cache &&
php artisan view:cache &&
php artisan migrate --force &&
ln -sfn /home/webservice/mng/releases/${RELEASE_ID} /home/webservice/mng/current &&
sudo systemctl reload php8.4-fpm &&
cd /home/webservice/mng/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
'
"""
}
}
}
// develop → Jenkins 관여 안함 (기존 post-update hook 유지)
}
post {
success {
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
message: "✅ *mng* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
}
failure {
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
message: "❌ *mng* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
script {
if (env.BRANCH_NAME == 'main') {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh ${DEPLOY_USER}@211.117.60.189 '
PREV=\$(ls -1dt /home/webservice/mng/releases/*/ | sed -n "2p" | xargs basename) &&
[ -n "\$PREV" ] && ln -sfn /home/webservice/mng/releases/\$PREV /home/webservice/mng/current &&
sudo systemctl reload php8.4-fpm
'
"""
}
}
}
}
}
}
수동 배포
ssh sam-prod
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
cd /home/webservice/mng/releases
git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.git $RELEASE_ID
ln -sfn /home/webservice/mng/shared/storage /home/webservice/mng/releases/$RELEASE_ID/storage
ln -sfn /home/webservice/mng/shared/.env /home/webservice/mng/releases/$RELEASE_ID/.env
cd /home/webservice/mng/releases/$RELEASE_ID
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs
composer install --no-dev --optimize-autoloader --no-interaction
# Vite 빌드 (Blade + Tailwind)
npm install --prefer-offline
npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
ln -sfn /home/webservice/mng/releases/$RELEASE_ID /home/webservice/mng/current
sudo systemctl reload php8.4-fpm
# 오래된 릴리즈 정리
cd /home/webservice/mng/releases
ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
Sales (Plain PHP) 배포
레거시 PHP 애플리케이션. rsync 기반 배포.
Jenkinsfile (sales/Jenkinsfile)
pipeline {
agent any
environment { DEPLOY_USER = 'hskwon' }
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Deploy Production') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
rsync -az --delete \
--exclude='.git' --exclude='.env' --exclude='storage' \
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/sales/
ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/sales && echo "sales deployed"'
"""
}
}
}
// develop → 개발서버는 기존 post-update hook 유지
}
post {
success { echo '✅ sales 배포 완료 (' + env.BRANCH_NAME + ')' }
failure { echo '❌ sales 배포 실패 (' + env.BRANCH_NAME + ')' }
}
}
수동 배포
ssh sam-prod
cd /home/webservice/sales
git pull origin main
별도 캐시나 빌드 절차 없음. .env 변경 시에만 주의.
Landing (정적 페이지) 배포
Jenkinsfile (landing/Jenkinsfile)
pipeline {
agent any
environment { DEPLOY_USER = 'hskwon' }
stages {
stage('Deploy Production') {
when { branch 'main' }
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/landing && git pull origin main'"
}
}
}
}
}
롤백
React 롤백
# 이전 릴리즈 확인
ssh sam-prod "ls -lt /home/webservice/react/releases/"
ssh sam-prod "readlink /home/webservice/react/current"
# 롤백 실행
ssh sam-prod "
PREV=\$(ls -1dt /home/webservice/react/releases/*/ | sed -n '2p' | xargs basename) &&
echo \"롤백 대상: \$PREV\" &&
ln -sfn /home/webservice/react/releases/\$PREV /home/webservice/react/current &&
cd /home/webservice && pm2 reload sam-front
"
API 롤백
ssh sam-prod "ls -1dt /home/webservice/api/releases/*/"
ssh sam-prod "
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n '2p' | xargs basename) &&
echo \"롤백 대상: \$PREV\" &&
ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
sudo systemctl reload php8.4-fpm &&
sudo supervisorctl restart sam-queue-worker:*
"
Jenkins 장애 시 수동 배포
React 수동 배포
# CI/CD 서버에서 빌드
cd /tmp
git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-react-prod.git react-build
cd react-build
cp /var/lib/jenkins/env-files/react/.env.main .env.local
npm install --prefer-offline
npm run build
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
# 운영서버로 전송
ssh sam-prod "mkdir -p /home/webservice/react/releases/${RELEASE_ID}"
rsync -az --delete \
.next package.json next.config.ts public node_modules \
hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/
scp .env.local hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local
# 심링크 전환 및 PM2 재시작
ssh sam-prod "
ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current &&
cd /home/webservice && pm2 reload sam-front
"
# 빌드 디렉토리 정리
rm -rf /tmp/react-build
API 수동 배포
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
ssh sam-prod "
cd /home/webservice/api/releases &&
git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} &&
ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage &&
ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env &&
cd /home/webservice/api/releases/${RELEASE_ID} &&
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
composer install --no-dev --optimize-autoloader --no-interaction &&
php artisan config:cache &&
php artisan route:cache &&
php artisan view:cache &&
php artisan migrate --force &&
ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current &&
sudo systemctl reload php8.4-fpm &&
sudo supervisorctl restart sam-queue-worker:*
"
배포 후 확인 사항
# 서비스 상태
sudo systemctl status nginx php8.4-fpm
pm2 status
sudo supervisorctl status
# 에러 로그
sudo tail -20 /var/log/nginx/api.sam.it.kr.error.log
sudo tail -20 /home/webservice/api/shared/storage/logs/laravel.log
# HTTP 응답 확인
curl -sI https://api.sam.it.kr
curl -sI https://sam.it.kr
curl -sI https://mng.codebridge-x.com
빌드 아티팩트 관리
# Jenkins workspace 용량 확인
sudo du -sh /var/lib/jenkins/workspace/*
# 운영서버 릴리즈 정리
ssh sam-prod "cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf"
ssh sam-prod "cd /home/webservice/api/releases && ls -1dt */ | tail -n +6 | xargs rm -rf"
# Jenkins 빌드 보관 정책: Jenkins > Job > Configure > Discard old builds
빌드 실패 조사
# Jenkins 로그에서 최근 오류
sudo journalctl -u jenkins --since "30 minutes ago" | grep -i error
# Jenkins workspace 확인
ls -la /var/lib/jenkins/workspace/
# 웹 콘솔 로그 (권장)
# https://ci.sam.it.kr/job/<JOB_NAME>/<BUILD_NUMBER>/console
빌드 실패 주요 원인:
- npm install 실패 -- node_modules 캐시, 네트워크
- npm run build 실패 -- TypeScript 오류, 환경변수 누락
- rsync 실패 -- SSH 키 문제, 디스크 공간 부족
- composer install 실패 -- 네트워크, PHP 확장 누락
- SSH 연결 실패 -- known_hosts 변경, 키 만료
- Laravel
package:discover실패 --bootstrap/cache/디렉토리 누락 (.gitignore에 포함) - Blade view 캐시 실패 --
storage/framework/views/디렉토리 누락 Target class [request] does not exist-- CLI 컨텍스트에서request()호출 (AppServiceProvider 확인)
Laravel 배포 필수:
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs를composer install전에 실행해야 함..gitignore가 이 디렉토리들을 제외하므로 rsync/git clone 후 생성 필요.