docs:배포 가이드 현행화 — 동시빌드방지, 승인알림, 환경파일 변경 반영

- 전체 Jenkinsfile에 disableConcurrentBuilds() 반영
- react/api Production Approval에 #product_deploy Slack 알림 추가
- react 환경파일 .env.local → .env.production 변경 반영
- Slack 알림 채널 테이블 추가 (#product_infra, #product_deploy)
- 환경변수 파일 테이블 DEV_TOOLBAR 컬럼 추가
- 수동 배포 섹션 .env.production 반영

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 11:37:03 +09:00
parent 4d9a044243
commit 46c5e23972

View File

@@ -22,6 +22,13 @@
| sam-manage | Laravel Admin 배포 | main | 운영 (직접) | | sam-manage | Laravel Admin 배포 | main | 운영 (직접) |
| sam-sales | 레거시 PHP 배포 | main | 운영 (직접) | | sam-sales | 레거시 PHP 배포 | main | 운영 (직접) |
### Slack 알림 채널
| 채널 | 용도 | 알림 내용 |
|------|------|----------|
| `#product_infra` | 빌드/배포 상태 | 빌드 시작, 배포 성공/실패 |
| `#product_deploy` | 운영 배포 승인 | Stage 배포 완료 후 승인 대기 알림 (Jenkins 승인 링크 포함) |
### 2-Branch 전략 (develop + main) ### 2-Branch 전략 (develop + main)
> **stage 브랜치 없음.** main 브랜치 push 시 Stage 자동 배포 → Jenkins 승인 → Production 배포. > **stage 브랜치 없음.** main 브랜치 push 시 Stage 자동 배포 → Jenkins 승인 → Production 배포.
@@ -35,7 +42,12 @@
1. 개발자가 develop → main 머지 후 push 1. 개발자가 develop → main 머지 후 push
2. post-receive hook → CI/CD Gitea 자동 push 2. post-receive hook → CI/CD Gitea 자동 push
3. Jenkins 빌드 → Stage 자동 배포 3. Jenkins 빌드 → Stage 자동 배포
4. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃) 4. `#product_deploy` Slack 채널에 승인 대기 알림 전송
5. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃)
> **동시 빌드 방지:** 모든 파이프라인에 `disableConcurrentBuilds()` 적용.
> 같은 프로젝트에서 빌드가 동시에 2개 이상 돌지 않음.
> 승인 대기 중 새 push 시 → 기존 빌드 Abort 후 새 빌드 자동 시작.
**main 브랜치 배포 흐름 (mng/sales):** **main 브랜치 배포 흐름 (mng/sales):**
1. 개발자가 main push → hook → CI/CD Gitea → Jenkins → Production 직접 배포 1. 개발자가 main push → hook → CI/CD Gitea → Jenkins → Production 직접 배포
@@ -141,6 +153,8 @@ Repository Settings → Webhooks → Add Webhook (Gitea)
│ │ │ │ │ │
│ ├─ Stage 자동 배포 (react: .env.stage 빌드) │ │ ├─ Stage 자동 배포 (react: .env.stage 빌드) │
│ │ │ │ │ │
│ ├─ 📢 #product_deploy Slack 알림 (승인 링크 포함) │
│ │ │
│ ├─ ⏸️ 승인 대기 (24시간 타임아웃) │ │ ├─ ⏸️ 승인 대기 (24시간 타임아웃) │
│ │ https://ci.sam.it.kr 에서 "운영 배포 진행" 클릭 │ │ │ https://ci.sam.it.kr 에서 "운영 배포 진행" 클릭 │
│ │ │ │ │ │
@@ -183,11 +197,11 @@ CI/CD Gitea push -> Webhook -> Jenkins
**환경변수 파일 (CI/CD 서버):** /var/lib/jenkins/env-files/react/ **환경변수 파일 (CI/CD 서버):** /var/lib/jenkins/env-files/react/
| 파일 | API URL | Frontend URL | APP_ENV | | 파일 | API URL | Frontend URL | APP_ENV | DEV_TOOLBAR |
|------|---------|-------------|---------| |------|---------|-------------|---------|-------------|
| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | development | | .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | development | - |
| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | staging | | .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | staging | - |
| .env.main | https://api.sam.it.kr | https://sam.it.kr | production | | .env.main | https://api.sam.it.kr | https://sam.it.kr | production | false |
> `NEXT_PUBLIC_APP_ENV` 값으로 타이틀 접두사 결정: `development` → `[D]`, `local` → `[L]`, 그 외 → 없음 > `NEXT_PUBLIC_APP_ENV` 값으로 타이틀 접두사 결정: `development` → `[D]`, `local` → `[L]`, 그 외 → 없음
@@ -201,6 +215,10 @@ CI/CD Gitea push -> Webhook -> Jenkins
pipeline { pipeline {
agent any agent any
options {
disableConcurrentBuilds()
}
environment { environment {
DEPLOY_USER = 'hskwon' DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
@@ -220,10 +238,10 @@ pipeline {
script { script {
if (env.BRANCH_NAME == 'main') { if (env.BRANCH_NAME == 'main') {
// main: Stage 빌드 먼저 (승인 후 Production 재빌드) // main: Stage 빌드 먼저 (승인 후 Production 재빌드)
sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.local" sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production"
} else { } else {
def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}"
sh "cp ${envFile} .env.local" sh "cp ${envFile} .env.production"
} }
} }
} }
@@ -247,7 +265,7 @@ pipeline {
--exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \ --exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \
.next package.json next.config.ts public node_modules \ .next package.json next.config.ts public node_modules \
${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/
scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local scp .env.production ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.production
ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react'
""" """
} }
@@ -264,7 +282,7 @@ pipeline {
rsync -az --delete \ rsync -az --delete \
.next package.json next.config.ts public node_modules \ .next package.json next.config.ts public node_modules \
${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ ${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 scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.production
ssh ${DEPLOY_USER}@211.117.60.189 ' ssh ${DEPLOY_USER}@211.117.60.189 '
ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && 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 && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 &&
@@ -279,6 +297,8 @@ pipeline {
stage('Production Approval') { stage('Production Approval') {
when { branch 'main' } when { branch 'main' }
steps { steps {
slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
message: "🔔 *react* 운영 배포 승인 대기 중\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
timeout(time: 24, unit: 'HOURS') { timeout(time: 24, unit: 'HOURS') {
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr',
ok: '운영 배포 진행' ok: '운영 배포 진행'
@@ -290,7 +310,7 @@ pipeline {
stage('Rebuild for Production') { stage('Rebuild for Production') {
when { branch 'main' } when { branch 'main' }
steps { steps {
sh "cp /var/lib/jenkins/env-files/react/.env.main .env.local" sh "cp /var/lib/jenkins/env-files/react/.env.main .env.production"
sh 'npm run build' sh 'npm run build'
} }
} }
@@ -305,7 +325,7 @@ pipeline {
rsync -az --delete \ rsync -az --delete \
.next package.json next.config.ts public node_modules \ .next package.json next.config.ts public node_modules \
${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ ${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 scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production
ssh ${DEPLOY_USER}@211.117.60.189 ' ssh ${DEPLOY_USER}@211.117.60.189 '
ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current &&
cd /home/webservice && pm2 reload sam-front && cd /home/webservice && pm2 reload sam-front &&
@@ -334,6 +354,10 @@ pipeline {
> Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다. > Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다.
> main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드. > main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드.
> **환경파일:** Jenkins는 CI/CD 서버의 env-files를 `.env.production`으로 복사하여 빌드한다.
> Next.js 우선순위: `.env.local` > `.env.production` > `.env`
> 따라서 서버에 `.env.local`이 있으면 `.env.production`을 덮어쓰므로 `.env.local`은 사용하지 않는다.
### PM2 수동 재시작 ### PM2 수동 재시작
```bash ```bash
@@ -374,6 +398,10 @@ CI/CD Gitea push -> Webhook -> Jenkins
pipeline { pipeline {
agent any agent any
options {
disableConcurrentBuilds()
}
environment { environment {
DEPLOY_USER = 'hskwon' DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
@@ -423,6 +451,8 @@ pipeline {
stage('Production Approval') { stage('Production Approval') {
when { branch 'main' } when { branch 'main' }
steps { steps {
slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
message: "🔔 *api* 운영 배포 승인 대기 중\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
timeout(time: 24, unit: 'HOURS') { timeout(time: 24, unit: 'HOURS') {
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr', input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr',
ok: '운영 배포 진행' ok: '운영 배포 진행'
@@ -583,6 +613,10 @@ API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Work
pipeline { pipeline {
agent any agent any
options {
disableConcurrentBuilds()
}
environment { environment {
DEPLOY_USER = 'hskwon' DEPLOY_USER = 'hskwon'
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
@@ -813,7 +847,7 @@ ssh sam-prod "
cd /tmp cd /tmp
git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-react-prod.git react-build git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-react-prod.git react-build
cd react-build cd react-build
cp /var/lib/jenkins/env-files/react/.env.main .env.local cp /var/lib/jenkins/env-files/react/.env.main .env.production
npm install --prefer-offline npm install --prefer-offline
npm run build npm run build
@@ -824,7 +858,7 @@ ssh sam-prod "mkdir -p /home/webservice/react/releases/${RELEASE_ID}"
rsync -az --delete \ rsync -az --delete \
.next package.json next.config.ts public node_modules \ .next package.json next.config.ts public node_modules \
hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ 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 scp .env.production hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production
# 심링크 전환 및 PM2 재시작 # 심링크 전환 및 PM2 재시작
ssh sam-prod " ssh sam-prod "