# 5. 배포 가이드 [목차로 돌아가기](./README.md) --- ## 파이프라인 개요 ### 전체 흐름 ``` 개발자 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):** 1. 개발자가 develop → main 머지 후 push 2. post-receive hook → CI/CD Gitea 자동 push 3. Jenkins 빌드 → Stage 자동 배포 4. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃) **main 브랜치 배포 흐름 (mng/sales):** 1. 개발자가 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/.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: - 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) ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') } stages { stage('Checkout') { steps { 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 { echo '✅ react 배포 완료 (' + env.BRANCH_NAME + ')' } failure { echo '❌ react 배포 실패 (' + env.BRANCH_NAME + ')' } } } ``` > **참고:** Next.js는 `NEXT_PUBLIC_*` 환경변수가 빌드 시 바인딩되므로, > Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다. > main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드. ### PM2 수동 재시작 ```bash 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) ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') } stages { stage('Checkout') { steps { 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} && 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} && 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 { echo "✅ api 배포 완료 (${env.BRANCH_NAME})" } failure { echo "❌ api 배포 실패 (${env.BRANCH_NAME})" 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로 전송하는 방식을 사용한다. ```bash 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. 의존성 설치 cd /home/webservice/api/releases/$RELEASE_ID composer install --no-dev --optimize-autoloader --no-interaction # 4. 캐시 생성 php artisan config:cache php artisan route:cache php artisan view:cache # 5. 마이그레이션 (필요시) php artisan migrate --force # 6. 심링크 전환 (이 시점에 배포 적용) ln -sfn /home/webservice/api/releases/$RELEASE_ID /home/webservice/api/current # 7. 서비스 리로드 sudo systemctl reload php8.4-fpm sudo supervisorctl restart sam-queue-worker:* # 8. 오래된 릴리즈 정리 (최근 5개만 유지) cd /home/webservice/api/releases ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true ``` ### 수동 배포 절차 (API Stage) ```bash 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 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) ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') } stages { stage('Checkout') { steps { 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} && 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 --production=false && 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 { echo "✅ mng 배포 완료 (${env.BRANCH_NAME})" } failure { echo "❌ mng 배포 실패 (${env.BRANCH_NAME})" 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 ' """ } } } } } } ``` ### 수동 배포 ```bash 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 composer install --no-dev --optimize-autoloader --no-interaction # Vite 빌드 (Blade + Tailwind) npm install --production=false 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) ```groovy 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 + ')' } } } ``` ### 수동 배포 ```bash ssh sam-prod cd /home/webservice/sales git pull origin main ``` 별도 캐시나 빌드 절차 없음. .env 변경 시에만 주의. --- ## Landing (정적 페이지) 배포 ### Jenkinsfile (landing/Jenkinsfile) ```groovy 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 롤백 ```bash # 이전 릴리즈 확인 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 롤백 ```bash 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 수동 배포 ```bash # 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 수동 배포 ```bash 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} && 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:* " ``` --- ## 배포 후 확인 사항 ```bash # 서비스 상태 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 ``` --- ## 빌드 아티팩트 관리 ```bash # 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 ``` --- ## 빌드 실패 조사 ```bash # 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///console ``` **빌드 실패 주요 원인:** 1. npm install 실패 -- node_modules 캐시, 네트워크 2. npm run build 실패 -- TypeScript 오류, 환경변수 누락 3. rsync 실패 -- SSH 키 문제, 디스크 공간 부족 4. composer install 실패 -- 네트워크, PHP 확장 누락 5. SSH 연결 실패 -- known_hosts 변경, 키 만료