Files
sam-docs/plans/production-deployment-plan.md
김보곤 c726e0852e docs: [plans] 운영 환경 배포 계획서 작성
- 12개 섹션 구성: 환경 전략, 서버 아키텍처, CI/CD, DB, 보안 등
- Jenkins CI/CD 파이프라인 설계 (4개 저장소)
- 단계별 마이그레이션 체크리스트 (Phase 1~4)
- INDEX.md에 문서 등록
2026-02-26 20:36:00 +09:00

37 KiB

SAM 운영 환경 배포 계획서

작성일: 2026-02-22 상태: 계획 수립 대상: MS3 정식 런칭 (2026-02-28) 작성자: 개발팀


1. 개요

1.1 목적

SAM 프로젝트의 MS3(정식 런칭, 2026-02-28)을 위해 개발 환경(dev.codebridge-x.com)에서 운영 환경(codebridge-x.com)으로의 전환을 체계적으로 수행한다. 수동 배포 방식에서 Jenkins CI/CD 기반 자동화 배포로 전환하여 안정적인 운영 체계를 구축한다.

1.2 핵심 원칙

  • 🔴 무중단 전환: 개발 환경 서비스에 영향 없이 운영 환경을 구축한다
  • 🔴 롤백 가능: 모든 배포는 즉시 롤백 가능해야 한다
  • 🔴 자동화 우선: 반복 작업은 Jenkins 파이프라인으로 자동화한다
  • 🟡 점진적 전환: 한 번에 전환하지 않고 Phase별로 검증한다

1.3 현재 환경 vs 목표 환경

항목 현재 (개발) 목표 (운영)
서버 114.203.209.83 (2코어/3.8GB) 신규 서버 (4코어/8GB 이상)
도메인 dev.codebridge-x.com codebridge-x.com
배포 방식 수동 (git pull + SSH) Jenkins CI/CD 자동화
SSL 자체 서명 인증서 Let's Encrypt
DB samdb (개발/운영 공용) sam_prod (운영 전용)
모니터링 없음 헬스체크 + Slack 알림
백업 수동 자동 일일 백업

1.4 관련 문서

문서 경로
런칭 로드맵 guides/project-launch-roadmap.md
.env 동기화 guides/production-env-sync.md
Docker 환경 스펙 specs/docker-setup.md
보안 정책 architecture/security-policy.md

2. 환경 전략

2.1 3-Tier 환경 분리

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   로컬 (WSL)     │     │   개발 서버       │     │   운영 서버       │
│   Docker 기반    │     │   Bare-metal     │     │   Bare-metal     │
│                 │     │                 │     │                 │
│ dev.sam.kr      │────→│ dev.codebridge  │────→│ codebridge-x    │
│ (hosts 매핑)    │     │ -x.com          │     │ .com            │
└─────────────────┘     └─────────────────┘     └─────────────────┘
  개발/테스트             스테이징/CI/CD          정식 서비스

2.2 도메인 매핑

서비스 로컬 (WSL Docker) 개발 서버 운영 서버
React (사용자) dev.sam.kr dev.codebridge-x.com codebridge-x.com
API api.sam.kr api.dev.codebridge-x.com api.codebridge-x.com
MNG (관리자) mng.sam.kr mng.dev.codebridge-x.com mng.codebridge-x.com
Sales sales.sam.kr sales.dev.codebridge-x.com sales.codebridge-x.com
5130 (레거시) 5130.sam.kr - -
Gitea - 114.203.209.83:3000 -

2.3 .env 분기 전략

상세 동기화 절차는 guides/production-env-sync.md 참조

환경 변수 로컬 (Docker) 개발 서버 운영 서버
APP_ENV local development production
APP_DEBUG true true false
APP_URL https://api.sam.kr https://api.dev.codebridge-x.com https://api.codebridge-x.com
DB_HOST sam-mysql-1 localhost localhost
DB_DATABASE samdb samdb sam_prod
LOG_CHANNEL stack stack stack
LOG_LEVEL debug debug warning
BAROBILL_TEST_MODE true true false

3. 운영 서버 아키텍처

3.1 서버 스펙 권장

항목 최소 사양 권장 사양 사유
CPU 4코어 8코어 PHP-FPM 3풀 + Node.js 동시 운영
RAM 8GB 16GB PHP-FPM 풀당 ~1.5GB + MySQL ~2GB
디스크 100GB SSD 200GB SSD DB + 로그 + 파일 스토리지
OS Ubuntu 22.04 LTS Ubuntu 24.04 LTS 장기 지원

경고: 현재 개발 서버(2코어/3.8GB)에서는 React 빌드 시 메모리 부족으로 실패한다. 운영 서버는 최소 8GB를 확보해야 한다.

3.2 Bare-metal 운영 결정

운영 서버는 Docker를 사용하지 않고 Bare-metal로 구성한다 (현재 개발 서버와 동일 방식).

항목 Docker Bare-metal (선택)
리소스 오버헤드 15~20% 없음
서버 스펙 요구 높음 낮음
운영 복잡도 중간 낮음
현재 개발 서버 - 이미 이 방식 사용 중

3.3 서비스 레이아웃

┌────────────────────────────────────────────────────────────┐
│                     운영 서버                               │
│                                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ Nginx (Reverse Proxy + Static Files)                │   │
│  │  :80 → HTTPS redirect                              │   │
│  │  :443 → PHP-FPM / Node.js                          │   │
│  └──────────┬──────────┬──────────┬────────────────────┘   │
│             │          │          │                         │
│  ┌──────────┴┐  ┌──────┴──────┐  ┌┴───────────┐           │
│  │ PHP-FPM   │  │ PHP-FPM     │  │ PHP-FPM    │           │
│  │ pool: api │  │ pool: mng   │  │ pool: sales│           │
│  │ :9001     │  │ :9002       │  │ :9003      │           │
│  └───────────┘  └─────────────┘  └────────────┘           │
│                                                            │
│  ┌──────────────┐  ┌──────────────────────────────┐        │
│  │ Node.js      │  │ Supervisor                   │        │
│  │ (React SSR)  │  │  - API Queue Worker (x1)     │        │
│  │ :3000        │  │  - MNG Queue Worker (x2)     │        │
│  └──────────────┘  │  - API Scheduler             │        │
│                    └──────────────────────────────┘        │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ MySQL 8.0 (sam_prod)                                 │  │
│  │ :3306 (localhost only)                               │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘

3.4 PHP-FPM 풀 설정

현재 Docker Supervisor 설정 기반으로 운영 서버 PHP-FPM 풀을 구성한다.

API 풀 (/etc/php/8.4/fpm/pool.d/api.conf):

[api]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm-api.sock
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 300
chdir = /home/webservice/api

MNG 풀 (/etc/php/8.4/fpm/pool.d/mng.conf):

[mng]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm-mng.sock
pm = dynamic
pm.max_children = 15
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500
request_terminate_timeout = 300
chdir = /home/webservice/mng

Sales 풀 (/etc/php/8.4/fpm/pool.d/sales.conf):

[sales]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm-sales.sock
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
chdir = /home/webservice/sales

3.5 Supervisor 프로세스 설정

현재 Docker 컨테이너의 supervisord.conf를 운영 서버용으로 변환한다.

API Queue Worker (/etc/supervisor/conf.d/sam-api-worker.conf):

[program:sam-api-worker]
command=php /home/webservice/api/artisan queue:work database --queue=api,default --sleep=3 --tries=3 --timeout=1800 --max-jobs=100 --max-time=3600
process_name=%(program_name)s_%(process_num)02d
numprocs=1
directory=/home/webservice/api
autostart=true
autorestart=true
startsecs=5
startretries=3
stopwaitsecs=1830
user=www-data
stdout_logfile=/var/log/sam/api-queue-worker.log
stdout_logfile_maxbytes=5MB
stderr_logfile=/var/log/sam/api-queue-worker-error.log
stderr_logfile_maxbytes=5MB

MNG Queue Worker (/etc/supervisor/conf.d/sam-mng-worker.conf):

[program:sam-mng-worker]
command=php /home/webservice/mng/artisan queue:work database --queue=mng,default --sleep=3 --tries=1 --timeout=1800 --max-jobs=10 --max-time=3600
process_name=%(program_name)s_%(process_num)02d
numprocs=2
directory=/home/webservice/mng
autostart=true
autorestart=true
startsecs=5
startretries=3
stopwaitsecs=1830
user=www-data
stdout_logfile=/var/log/sam/mng-queue-worker.log
stdout_logfile_maxbytes=5MB
stderr_logfile=/var/log/sam/mng-queue-worker-error.log
stderr_logfile_maxbytes=5MB

API Scheduler (/etc/supervisor/conf.d/sam-api-scheduler.conf):

[program:sam-api-scheduler]
command=bash -c "while true; do php /home/webservice/api/artisan schedule:run --no-interaction; sleep 60; done"
process_name=%(program_name)s
numprocs=1
directory=/home/webservice/api
autostart=true
autorestart=true
startsecs=0
user=www-data
stdout_logfile=/var/log/sam/api-scheduler.log
stdout_logfile_maxbytes=5MB
stderr_logfile=/var/log/sam/api-scheduler-error.log
stderr_logfile_maxbytes=5MB

3.6 필수 패키지 설치

현재 Docker Dockerfile 기반으로 운영 서버에 설치할 패키지 목록:

# PHP 8.4 + 확장 모듈 (API + MNG 공통)
apt install php8.4-fpm php8.4-mysql php8.4-zip php8.4-intl \
            php8.4-xml php8.4-soap php8.4-mbstring php8.4-curl

# MNG 전용 (GD + LibreOffice + FFmpeg)
apt install php8.4-gd libreoffice-writer-nogui \
            fonts-nanum fonts-nanum-extra ffmpeg

# Node.js 20 LTS (React SSR)
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install nodejs

# 기타
apt install nginx mysql-server supervisor git unzip

4. Jenkins CI/CD 파이프라인

4.1 Jenkins 설치 위치

Jenkins는 **개발 서버(114.203.209.83)**에 설치한다. 서버 메모리 한계를 고려하여 Swap을 추가한다.

# Swap 4GB 추가
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# Jenkins 설치
wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo apt-key add -
echo "deb https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list
sudo apt update && sudo apt install jenkins

4.2 Gitea Webhook 연동

각 저장소에서 Push 이벤트 발생 시 Jenkins 빌드가 자동 트리거된다.

저장소 Gitea URL Jenkins Job
sam-api http://114.203.209.83:3000/SamProject/sam-api.git sam-api-deploy
sam-manage http://114.203.209.83:3000/SamProject/sam-manage.git sam-mng-deploy
sam-react-prod http://114.203.209.83:3000/SamProject/sam-react-prod.git sam-react-deploy
sam-sales http://114.203.209.83:3000/SamProject/sam-sales.git sam-sales-deploy
sam-docs http://114.203.209.83:3000/SamProject/sam-docs.git - (배포 없음)

4.3 브랜치 전략

feature/* ──→ develop ──→ main/master
              (자동배포)    (승인 후 배포)
              ↓              ↓
         개발 서버        운영 서버
브랜치 배포 대상 트리거 승인
develop 개발 서버 Push 자동 불필요
main/master 운영 서버 PR 머지 팀장 승인 필수

4.4 저장소별 Jenkinsfile

sam-api 파이프라인

pipeline {
    agent any

    environment {
        DEPLOY_SERVER = credentials('prod-server-ssh')
        DEPLOY_PATH = '/home/webservice/api'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Lint') {
            steps {
                sh 'composer install --no-interaction'
                sh './vendor/bin/pint --test'
            }
        }

        stage('Test') {
            steps {
                sh 'php artisan test --parallel'
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sshagent(['prod-server-ssh']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} '
                            cd ${DEPLOY_PATH} &&
                            git pull origin main &&
                            composer install --no-dev --optimize-autoloader &&
                            php artisan migrate --force &&
                            php artisan config:clear &&
                            php artisan cache:clear &&
                            php artisan route:cache &&
                            php artisan view:cache &&
                            sudo supervisorctl restart sam-api-worker:*
                        '
                    """
                }
            }
        }
    }

    post {
        success {
            slackSend channel: '#sam-deploy',
                      message: "API 배포 성공: ${env.BUILD_URL}"
        }
        failure {
            slackSend channel: '#sam-alerts',
                      message: "API 배포 실패: ${env.BUILD_URL}"
        }
    }
}

sam-manage 파이프라인

pipeline {
    agent any

    environment {
        DEPLOY_SERVER = credentials('prod-server-ssh')
        DEPLOY_PATH = '/home/webservice/mng'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Lint') {
            steps {
                sh 'composer install --no-interaction'
                sh './vendor/bin/pint --test'
            }
        }

        stage('Build Assets') {
            steps {
                sh 'npm ci && npx tailwindcss -o public/css/app.css --minify'
            }
        }

        stage('Deploy') {
            when {
                branch 'master'
            }
            steps {
                sshagent(['prod-server-ssh']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} '
                            cd ${DEPLOY_PATH} &&
                            git pull origin master &&
                            composer install --no-dev --optimize-autoloader &&
                            php artisan config:clear &&
                            php artisan cache:clear &&
                            php artisan view:cache &&
                            sudo supervisorctl restart sam-mng-worker:*
                        '
                    """
                }
            }
        }
    }

    post {
        success {
            slackSend channel: '#sam-deploy',
                      message: "MNG 배포 성공: ${env.BUILD_URL}"
        }
        failure {
            slackSend channel: '#sam-alerts',
                      message: "MNG 배포 실패: ${env.BUILD_URL}"
        }
    }
}

참고: MNG는 마이그레이션을 실행하지 않는다. 모든 마이그레이션은 API에서만 실행한다.

sam-react-prod 파이프라인

pipeline {
    agent any

    environment {
        DEPLOY_SERVER = credentials('prod-server-ssh')
        DEPLOY_PATH = '/home/webservice/react'
        BUILD_FILE = 'next-standalone.tar.gz'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }

        stage('Build') {
            steps {
                sh '''
                    # .env.local 백업 (.env.production 으로 빌드)
                    [ -f .env.local ] && mv .env.local .env.local.bak

                    npm run build

                    # .env.local 복원
                    [ -f .env.local.bak ] && mv .env.local.bak .env.local

                    # standalone 빌드 확인
                    test -f .next/standalone/server.js
                '''
            }
        }

        stage('Package') {
            steps {
                sh """
                    rm -f ${BUILD_FILE}
                    COPYFILE_DISABLE=1 tar -czf ${BUILD_FILE} \
                        .next/standalone \
                        .next/static \
                        public
                """
            }
        }

        stage('Deploy') {
            when {
                branch 'master'
            }
            steps {
                sshagent(['prod-server-ssh']) {
                    sh """
                        scp ${BUILD_FILE} ${DEPLOY_SERVER}:${DEPLOY_PATH}/
                        ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} '
                            cd ${DEPLOY_PATH} &&
                            lsof -ti:3000 | xargs kill 2>/dev/null || true &&
                            sleep 2 &&
                            rm -rf .next.bak &&
                            mv .next .next.bak 2>/dev/null || true &&
                            tar xzf ${BUILD_FILE} &&
                            cp -r .next/static .next/standalone/.next/static &&
                            cp -r public .next/standalone/public &&
                            cp .env.production .next/standalone/.env.production 2>/dev/null || true &&
                            cd .next/standalone &&
                            PORT=3000 HOSTNAME=0.0.0.0 nohup node server.js > /tmp/sam-react.log 2>&1 & &&
                            sleep 3 &&
                            cd ${DEPLOY_PATH} &&
                            rm -f ${BUILD_FILE} &&
                            rm -rf .next.bak
                        '
                    """
                }
            }
        }
    }

    post {
        success {
            slackSend channel: '#sam-deploy',
                      message: "React 배포 성공: ${env.BUILD_URL}"
        }
        failure {
            slackSend channel: '#sam-alerts',
                      message: "React 배포 실패: ${env.BUILD_URL}"
        }
    }
}

경고: React 빌드는 Jenkins 서버(Swap 추가 후)에서 수행한다. Jenkins 서버에서도 메모리 부족 시 로컬(WSL)에서 빌드 후 deploy.sh로 배포한다.

sam-sales 파이프라인 (간소화)

pipeline {
    agent any

    stages {
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sshagent(['prod-server-ssh']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} '
                            cd /home/webservice/sales &&
                            git pull origin main &&
                            composer install --no-dev --optimize-autoloader &&
                            php artisan config:clear &&
                            php artisan cache:clear
                        '
                    """
                }
            }
        }
    }
}

5. 데이터베이스 전략

5.1 개발/운영 DB 물리 분리

항목 개발 DB 운영 DB
DB명 samdb sam_prod
위치 개발 서버 (114.203.209.83) 운영 서버
접속 samuser/sampass 별도 운영 계정
용도 개발/테스트 정식 서비스

5.2 마이그레이션 규칙

경고: 모든 마이그레이션은 API 프로젝트(/home/webservice/api)에서만 실행한다. MNG에서 마이그레이션 실행 금지.

# 운영 서버 마이그레이션 (API에서만)
cd /home/webservice/api
php artisan migrate --force

5.3 초기 데이터 마이그레이션 절차

# 1. 개발 DB 덤프 (구조 + 필수 데이터)
mysqldump -u samuser -p samdb \
    --single-transaction \
    --routines \
    --triggers \
    --add-drop-table \
    > sam_initial_dump.sql

# 2. 운영 서버로 전송
scp sam_initial_dump.sql user@prod-server:/tmp/

# 3. 운영 DB에 복원
mysql -u sam_prod_user -p sam_prod < /tmp/sam_initial_dump.sql

# 4. 운영 전용 설정 적용
mysql -u sam_prod_user -p sam_prod << 'EOF'
-- 바로빌 운영 모드 전환
UPDATE barobill_configs SET is_active = 0 WHERE environment = 'test';
UPDATE barobill_configs SET is_active = 1 WHERE environment = 'production';
UPDATE barobill_members SET server_mode = 'production';

-- 테스트 데이터 정리 (필요 시)
-- DELETE FROM ... WHERE is_test = 1;
EOF

5.4 백업 체계

항목 주기 보관 기간 방법
전체 백업 매일 03:00 30일 mysqldump --single-transaction
증분 백업 매 6시간 7일 mysqlbinlog
배포 전 스냅샷 배포 시 다음 배포까지 Jenkins 파이프라인 내 자동 실행

자동 백업 스크립트 (/etc/cron.d/sam-backup):

# 매일 03:00 전체 백업
0 3 * * * root /home/webservice/scripts/db-backup.sh >> /var/log/sam/backup.log 2>&1
#!/bin/bash
# /home/webservice/scripts/db-backup.sh
BACKUP_DIR="/home/webservice/backups/db"
DATE=$(date +%Y%m%d_%H%M%S)
KEEP_DAYS=30

mkdir -p ${BACKUP_DIR}

mysqldump -u sam_prod_user --single-transaction --routines --triggers sam_prod \
    | gzip > ${BACKUP_DIR}/sam_prod_${DATE}.sql.gz

# 30일 이상 된 백업 삭제
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${KEEP_DAYS} -delete

# Slack 알림
curl -X POST -H 'Content-type: application/json' \
    --data "{\"text\":\"DB 백업 완료: sam_prod_${DATE}.sql.gz\"}" \
    ${SLACK_WEBHOOK_URL}

6. SSL/도메인 설정

6.1 Let's Encrypt 인증서 발급

# Certbot 설치
apt install certbot python3-certbot-nginx

# 인증서 발급 (4개 도메인)
certbot --nginx -d codebridge-x.com \
                -d api.codebridge-x.com \
                -d mng.codebridge-x.com \
                -d sales.codebridge-x.com

# 자동 갱신 확인
certbot renew --dry-run

# 자동 갱신 cron (이미 certbot이 자동 설정)
# /etc/cron.d/certbot

6.2 Nginx 운영 설정

현재 Docker Nginx 설정(docker/nginx/nginx.conf)을 기반으로 운영 서버용으로 변환한다.

핵심 변경 사항:

항목 개발 (Docker) 운영 (Bare-metal)
upstream proxy_pass http://react:3000 proxy_pass http://127.0.0.1:3000
PHP-FPM fastcgi_pass api:9000 fastcgi_pass unix:/run/php/php8.4-fpm-api.sock
SSL 자체 서명 Let's Encrypt
도메인 *.sam.kr *.codebridge-x.com

보안 헤더 (개발 서버 Sales 설정 기반):

# 공통 보안 헤더
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# 보안: 악의적 경로 패턴 차단
if ($request_uri ~* "(\.\.\/|\.\.\\|etc\/passwd|\.env|\.git|\.htaccess|\.sql|@fs\/)") {
    return 403;
}

# 보안: 의심스러운 User-Agent 차단
if ($http_user_agent ~* "(sqlmap|nikto|nmap|masscan|metasploit|nessus)") {
    return 403;
}

7. 모니터링 및 로깅

7.1 로그 집중화

/var/log/sam/
├── api-laravel.log          # API Laravel 로그 (심볼릭 링크)
├── mng-laravel.log          # MNG Laravel 로그 (심볼릭 링크)
├── api-queue-worker.log     # API Queue Worker
├── api-queue-worker-error.log
├── mng-queue-worker.log     # MNG Queue Worker
├── mng-queue-worker-error.log
├── api-scheduler.log        # API Scheduler
├── react.log                # React SSR 로그
├── backup.log               # DB 백업 로그
└── healthcheck.log          # 헬스체크 로그
# 심볼릭 링크 설정
ln -sf /home/webservice/api/storage/logs/laravel.log /var/log/sam/api-laravel.log
ln -sf /home/webservice/mng/storage/logs/laravel.log /var/log/sam/mng-laravel.log

7.2 헬스체크 스크립트

5분 주기 실행 (/etc/cron.d/sam-healthcheck):

*/5 * * * * root /home/webservice/scripts/healthcheck.sh >> /var/log/sam/healthcheck.log 2>&1
#!/bin/bash
# /home/webservice/scripts/healthcheck.sh

SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
SERVICES=(
    "https://codebridge-x.com|React"
    "https://api.codebridge-x.com/up|API"
    "https://mng.codebridge-x.com|MNG"
    "https://sales.codebridge-x.com|Sales"
)

for service in "${SERVICES[@]}"; do
    IFS='|' read -r url name <<< "$service"
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url")

    if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 302 ]; then
        echo "[$(date)] ALERT: ${name} DOWN (HTTP ${HTTP_CODE})"
        curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"${name} 서비스 다운! HTTP ${HTTP_CODE} - ${url}\"}" \
            "$SLACK_WEBHOOK"
    fi
done

# MySQL 체크
if ! mysqladmin ping -u sam_prod_user --silent 2>/dev/null; then
    echo "[$(date)] ALERT: MySQL DOWN"
    curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"MySQL 서비스 다운!"}' \
        "$SLACK_WEBHOOK"
fi

# 디스크 사용량 체크 (90% 이상 경고)
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_USAGE" -ge 90 ]; then
    curl -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"디스크 사용량 경고: ${DISK_USAGE}%\"}" \
        "$SLACK_WEBHOOK"
fi

7.3 Slack 채널 구성

채널 용도 알림 내용
#sam-deploy 배포 알림 배포 성공/실패 결과
#sam-alerts 장애 알림 서비스 다운, 디스크 부족, DB 연결 실패
#sam-errors 에러 로그 Laravel 500 에러, Queue 실패

8. 롤백 전략

8.1 API/MNG 롤백

# 1. 이전 커밋으로 코드 복원
cd /home/webservice/api
git log --oneline -5                    # 이전 커밋 확인
git checkout <이전_커밋_해시>

# 2. 의존성 복원
composer install --no-dev --optimize-autoloader

# 3. 캐시 초기화
php artisan config:clear
php artisan cache:clear
php artisan route:cache

# 4. Queue Worker 재시작
sudo supervisorctl restart sam-api-worker:*

8.2 React 롤백

# .next.bak 이 남아있는 경우 (배포 직후)
cd /home/webservice/react
lsof -ti:3000 | xargs kill 2>/dev/null || true
rm -rf .next
mv .next.bak .next
cd .next/standalone
PORT=3000 HOSTNAME=0.0.0.0 nohup node server.js > /tmp/sam-react.log 2>&1 &

8.3 DB 롤백

우선순위 방법 설명
1순위 코드 롤백 마이그레이션 문제가 아니면 코드만 롤백
2순위 migrate:rollback 마지막 마이그레이션 배치 되돌리기
3순위 스냅샷 복원 배포 전 자동 스냅샷에서 복원
# 마이그레이션 롤백
cd /home/webservice/api
php artisan migrate:rollback --step=1

# 스냅샷 복원 (최후의 수단)
mysql -u sam_prod_user -p sam_prod < /home/webservice/backups/db/pre-deploy-snapshot.sql.gz

9. 보안 강화

9.1 방화벽 (UFW)

# 기본 정책
ufw default deny incoming
ufw default allow outgoing

# 허용 포트
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP
ufw allow 443/tcp   # HTTPS

# MySQL은 localhost만 (외부 차단)
# 기본 deny에 의해 자동 차단됨

# 활성화
ufw enable

9.2 SSH 보안

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers deploy

9.3 fail2ban

apt install fail2ban

# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 22
maxretry = 5
bantime = 3600

[nginx-http-auth]
enabled = true

[nginx-limit-req]
enabled = true

9.4 운영 .env 관리 규칙

❌ .env 파일을 Git에 커밋 금지
❌ .env 파일을 Slack/메신저로 공유 금지
✅ 서버에서 직접 편집 (vi /home/webservice/api/.env)
✅ 변경 시 팀 채널에 "어떤 키를 변경했는지"만 공유

9.5 보안 체크리스트

# 항목 확인
1 APP_DEBUG=false [ ]
2 APP_ENV=production [ ]
3 APP_KEY 운영 전용 키 생성 [ ]
4 DB 비밀번호 강력한 값으로 변경 [ ]
5 MySQL 외부 접속 차단 (bind-address=127.0.0.1) [ ]
6 UFW 방화벽 활성화 [ ]
7 SSH 키 인증만 허용 (비밀번호 금지) [ ]
8 fail2ban 설치 및 활성화 [ ]
9 Nginx 보안 헤더 적용 (HSTS, X-Frame 등) [ ]
10 Nginx 악의적 경로/UA 차단 규칙 적용 [ ]
11 SSL 인증서 발급 및 자동 갱신 설정 [ ]
12 .env 파일 권한 600 설정 [ ]
13 storage/, bootstrap/cache/ 권한 확인 [ ]
14 phpMyAdmin 운영 서버에 설치하지 않음 [ ]
15 Sanctum 토큰 만료 시간 설정 확인 [ ]
16 LOG_SLACK_WEBHOOK_URL 설정 (에러 알림) [ ]

10. 단계별 마이그레이션 체크리스트

10.1 Phase 1: 인프라 구축 (1주)

# 작업 담당 확인
1 운영 서버 호스팅 계약 및 OS 설치 팀장 [ ]
2 기본 패키지 설치 (Nginx, PHP 8.4, MySQL 8.0, Node.js 20) 팀장 [ ]
3 PHP 확장 모듈 설치 (zip, intl, xml, soap, gd 등) 팀장 [ ]
4 LibreOffice, FFmpeg 설치 (MNG용) 팀장 [ ]
5 Supervisor 설치 및 설정 팀장 [ ]
6 MySQL sam_prod 데이터베이스 생성 팀장 [ ]
7 MySQL 운영 계정 생성 (외부 접속 차단) 팀장 [ ]
8 UFW 방화벽 설정 (22, 80, 443만 허용) 팀장 [ ]
9 SSH 키 인증 설정 (비밀번호 로그인 차단) 팀장 [ ]
10 fail2ban 설치 팀장 [ ]
11 DNS 레코드 추가 (A 레코드 4개) 팀장 [ ]
12 Let's Encrypt SSL 인증서 발급 팀장 [ ]
13 Nginx 운영 설정 배포 (4개 도메인) 팀장 [ ]
14 PHP-FPM 3개 풀 설정 (api, mng, sales) 팀장 [ ]
15 로그 디렉토리 생성 (/var/log/sam/) 팀장 [ ]
16 백업 스크립트 설치 및 cron 등록 팀장 [ ]

10.2 Phase 2: CI/CD 파이프라인 구축 (1주)

# 작업 담당 확인
1 개발 서버 Swap 4GB 추가 팀장 [ ]
2 Jenkins 설치 및 초기 설정 팀장 [ ]
3 Gitea → Jenkins Webhook 연동 (4개 저장소) 팀장 [ ]
4 Jenkins SSH Credential 등록 (운영 서버) 팀장 [ ]
5 sam-api Jenkinsfile 작성 및 테스트 팀장 [ ]
6 sam-manage Jenkinsfile 작성 및 테스트 팀장 [ ]
7 sam-react-prod Jenkinsfile 작성 및 테스트 팀장 [ ]
8 sam-sales Jenkinsfile 작성 및 테스트 팀장 [ ]
9 Slack Webhook 연동 (배포/장애 알림) 팀장 [ ]
10 헬스체크 스크립트 설치 및 cron 등록 팀장 [ ]
11 develop → 개발 서버 자동 배포 테스트 팀장 [ ]

10.3 Phase 3: 스테이징 배포 (3일)

# 작업 담당 확인
1 프로젝트 소스 코드 클론 (4개 저장소) 팀장 [ ]
2 운영 .env 파일 생성 (API, MNG, Sales, React) 팀장 [ ]
3 composer install (API, MNG, Sales) 팀장 [ ]
4 개발 DB → 운영 DB 데이터 마이그레이션 팀장 [ ]
5 php artisan migrate --force (API에서만) 팀장 [ ]
6 바로빌 운영 설정 전환 (DB + .env) 팀장 [ ]
7 Google 서비스 어카운트 파일 배치 팀장 [ ]
8 React 빌드 및 배포 (standalone) 팀장 [ ]
9 Supervisor 프로세스 시작 팀장 [ ]
10 전체 서비스 기동 확인 전원 [ ]
11 기능 테스트 (로그인, 견적, 세금계산서 등) 전원 [ ]
12 외부 서비스 연동 확인 (바로빌, FCM, Gemini) 전원 [ ]
13 성능 기본 테스트 (응답 속도 < 500ms) 팀장 [ ]

10.4 Phase 4: 운영 전환 (1일)

# 작업 담당 확인
1 전환 일시 공지 (사용자/팀) 팀장 [ ]
2 개발 DB 최종 덤프 → 운영 DB 동기화 팀장 [ ]
3 DNS 최종 전환 (운영 서버 IP로 변경) 팀장 [ ]
4 SSL 인증서 최종 확인 팀장 [ ]
5 운영 환경 최종 기동 팀장 [ ]
6 Jenkins 운영 파이프라인 활성화 팀장 [ ]
7 모니터링/헬스체크 최종 확인 팀장 [ ]
8 사용자 접속 안내 (URL 변경) 팀장 [ ]
9 2시간 집중 모니터링 전원 [ ]
10 전환 완료 공지 팀장 [ ]

운영 전환 후 검증 체크리스트:

□ React 메인 페이지 로딩 확인
□ 로그인/로그아웃 정상
□ MNG 관리자 화면 접속 확인
□ API 엔드포인트 응답 확인 (/up)
□ 파일 업로드/다운로드 정상
□ 바로빌 세금계산서 발행 테스트
□ FCM 푸시 알림 전송 확인
□ Queue Worker 정상 동작 (failed_jobs 확인)
□ Scheduler 정상 동작
□ Slack 알림 수신 확인

11. 일정 요약

Week 1 (02/22~02/28)     Week 2 (03/01~03/07)     Week 3 (03/08~03/14)
     │                        │                         │
 Phase 1                  Phase 2                  Phase 3 → Phase 4
 인프라 구축              CI/CD 구축                스테이징    운영 전환
 ├─ 서버 셋업             ├─ Jenkins 설치           ├─ 데이터    ├─ DNS 전환
 ├─ 패키지 설치           ├─ Webhook 연동           │  마이그    ├─ 모니터링
 ├─ 방화벽/SSL            ├─ 파이프라인 작성         │  레이션    └─ 전환 공지
 └─ Nginx 설정            └─ Slack 연동             └─ 기능
                                                      테스트

참고: MS3 목표일(2026-02-28)은 Phase 1 완료 시점이다. 실제 운영 전환은 Week 3에 수행한다. 필요 시 Phase 1~2를 병렬로 진행하여 일정을 단축할 수 있다.


12. 위험 요소 및 완화 방안

# 위험 요소 영향도 발생 확률 완화 방안
1 RAM 부족으로 React 빌드 실패 🔴 높음 중간 Jenkins 서버 Swap 추가, 폴백으로 로컬 빌드 사용
2 DNS 전파 지연 🟡 중간 높음 TTL 사전 단축 (300초), 전환 24시간 전 TTL 변경
3 바로빌 운영 전환 실패 🔴 높음 낮음 DB + .env 동시 전환, 즉시 롤백 절차 준비 (production-env-sync.md 참조)
4 운영 서버 호스팅 지연 🔴 높음 낮음 대안 호스팅 사전 조사, 최소 1주 여유 확보
5 Jenkins 메모리 부족 🟡 중간 중간 Swap 4GB 추가, 동시 빌드 제한 (1개), 빌드 후 workspace 정리
6 마이그레이션 충돌 🟡 중간 낮음 배포 전 DB 스냅샷, migrate:rollback 준비
7 Google 서비스 어카운트 경로 불일치 🟡 중간 중간 운영 서버 경로 통일, .env 교차 검증

관련 문서


최종 업데이트: 2026-02-22