diff --git a/dev/deploys/ops-manual/01-server-overview.md b/dev/deploys/ops-manual/01-server-overview.md index bd77643..a647b3f 100644 --- a/dev/deploys/ops-manual/01-server-overview.md +++ b/dev/deploys/ops-manual/01-server-overview.md @@ -44,6 +44,7 @@ | Redis | 7.0.15 | 6379 (localhost) | active | | PM2 | 6.0.14 | 3000 (cluster x2), 3100 (fork x1) | active | | Supervisor | - | - | active (queue worker x2) | +| Laravel Scheduler | - | - | cron (API + MNG, www-data) | | node_exporter | 1.8.2 | 9100 | active | | Certbot | 2.9.0 | - | timer active | | fail2ban | - | - | active | @@ -133,10 +134,10 @@ | IP | 110.10.147.46 | | SSH 별칭 | sam-cicd | | OS | Ubuntu 24.04.4 LTS | -| Kernel | 6.8.0-41-generic | -| CPU | 4 vCPU | -| RAM | 8GB (Swap 4GB) | -| Disk | 98GB (사용 15GB / 여유 79GB) | +| Kernel | 6.8.0-106-generic | +| CPU | 8 vCPU | +| RAM | 16GB (Swap 4GB) | +| Disk | 98GB (사용 32GB / 여유 61GB) | | 사용자 | hskwon(개발팀장), pro(개발실장/잠금), kkk(개발자/잠금) | ### 도메인 매핑 @@ -166,14 +167,15 @@ | 서비스 | 할당 | 설정 | |--------|------|------| -| Jenkins | ~2.0GB | -Xmx2048m | -| MySQL | ~1.5GB | innodb_buffer_pool_size=1536M | -| Gitea | ~0.5GB | Go 기반 | -| Prometheus | ~0.5GB | retention 30d | +| Jenkins | ~1.2GB | -Xmx2048m | +| Jenkins Agent | ~0.3GB | local-agent (WebSocket) | +| MySQL | ~0.7GB | innodb_buffer_pool_size=1536M | +| Gitea | ~0.2GB | Go 기반 | +| Prometheus | ~0.1GB | retention 30d | | Grafana | ~0.3GB | - | | Nginx | ~0.1GB | - | | node_exporter | ~10MB | - | -| OS + 여유 | ~3.1GB | Swap 4GB | +| OS + 여유 | ~13GB | Swap 4GB | ### 주요 설정 파일 @@ -194,8 +196,9 @@ | MySQL 튜닝 | /etc/mysql/mysql.conf.d/sam-tuning.cnf | | fail2ban 설정 | /etc/fail2ban/ | | SSL 인증서 | /etc/letsencrypt/live/ | -| 백업 스크립트 | /home/hskwon/scripts/backup-db.sh | -| 백업 저장소 | /home/hskwon/backups/mysql/ | +| 백업 스크립트 | /data/scripts/backup-db.sh | +| 백업 인증 | /data/scripts/.sam_backup.cnf | +| 백업 저장소 | /data/backups/mysql/ | ### 방화벽 (UFW) 규칙 @@ -301,7 +304,7 @@ ``` ┌──────────────────────────────────────────────────────────┐ -│ CI/CD서버 (2 vCPU / 8GB) │ +│ CI/CD서버 (8 vCPU / 16GB) │ │ Ubuntu 24.04 / IP: 110.10.147.46 │ │ │ │ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │ diff --git a/dev/deploys/ops-manual/03-service-prod.md b/dev/deploys/ops-manual/03-service-prod.md index 965cebc..41ae5f0 100644 --- a/dev/deploys/ops-manual/03-service-prod.md +++ b/dev/deploys/ops-manual/03-service-prod.md @@ -226,6 +226,48 @@ sudo supervisorctl update --- +## Laravel Scheduler (cron) + +API와 MNG의 스케줄 작업을 `/etc/crontab`에서 www-data로 실행. + +**등록 위치:** `/etc/crontab` + +```crontab +# Laravel Scheduler - API +* * * * * www-data cd /home/webservice/api/current && php artisan schedule:run >> /dev/null 2>&1 + +# Laravel Scheduler - MNG +* * * * * www-data cd /home/webservice/mng/current && php artisan schedule:run >> /dev/null 2>&1 +``` + +**등록된 스케줄 확인:** + +```bash +# API 스케줄 목록 +cd /home/webservice/api/current && php artisan schedule:list + +# MNG 스케줄 목록 +cd /home/webservice/mng/current && php artisan schedule:list +``` + +**주요 스케줄:** + +| 프로젝트 | 명령 | 주기 | 설명 | +|---------|------|------|------| +| API | `api-log:prune` | 매일 03:00 | API 로그 정리 | +| API | `audit:prune` | 매일 03:10 | 감사 로그 정리 | +| API | `stat:aggregate-daily` | 매일 02:00 | 일별 통계 집계 | +| API | `sanctum:prune-expired` | 매일 03:20 | 만료 토큰 정리 | +| API | `storage:cleanup-*` | 매일 03:30~50 | 스토리지 정리 | +| API | `stat:check-kpi-alerts` | 매일 09:00 | KPI 알림 체크 | +| MNG | `attendance:mark-absent` | 매일 23:50 | 미출근 자동 처리 | +| MNG | `barobill:sync-cards` | 2시간마다 | 바로빌 카드거래 동기화 | + +> **주의:** 실행 계정은 반드시 `www-data`여야 합니다. 개인 계정(hskwon 등)으로 실행하면 +> storage/logs 파일 권한 문제로 500 에러가 발생합니다. + +--- + ## node_exporter ```bash diff --git a/dev/deploys/ops-manual/05-deployment.md b/dev/deploys/ops-manual/05-deployment.md index eb9d2ab..656abcb 100644 --- a/dev/deploys/ops-manual/05-deployment.md +++ b/dev/deploys/ops-manual/05-deployment.md @@ -657,9 +657,12 @@ pipeline { ssh ${DEPLOY_USER}@211.117.60.189 ' cd /home/webservice/mng/releases/${RELEASE_ID} && mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} && + rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs && ln -sfn /home/webservice/mng/shared/.env .env && + sudo chmod 640 /home/webservice/mng/shared/.env && ln -sfn /home/webservice/mng/shared/storage/app storage/app && - ln -sfn /home/webservice/mng/shared/storage/logs storage/logs && + ln -sfn /home/webservice/mng/shared/storage/credentials storage/credentials && + rm -rf storage/fonts && ln -sfn /home/webservice/mng/shared/storage/fonts storage/fonts && composer install --no-dev --optimize-autoloader --no-interaction && npm install --prefer-offline && npm run build && @@ -716,7 +719,11 @@ git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.gi ln -sfn /home/webservice/mng/shared/.env /home/webservice/mng/releases/$RELEASE_ID/.env ln -sfn /home/webservice/mng/shared/storage/app /home/webservice/mng/releases/$RELEASE_ID/storage/app +rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/logs ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/releases/$RELEASE_ID/storage/logs +ln -sfn /home/webservice/mng/shared/storage/credentials /home/webservice/mng/releases/$RELEASE_ID/storage/credentials +rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/fonts +ln -sfn /home/webservice/mng/shared/storage/fonts /home/webservice/mng/releases/$RELEASE_ID/storage/fonts cd /home/webservice/mng/releases/$RELEASE_ID mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} diff --git a/dev/deploys/ops-manual/06-database.md b/dev/deploys/ops-manual/06-database.md index c31a6da..c004f27 100644 --- a/dev/deploys/ops-manual/06-database.md +++ b/dev/deploys/ops-manual/06-database.md @@ -4,6 +4,38 @@ --- +## 마이그레이션 정책 + +### 원칙 + +| 대상 DB | 마이그레이션 위치 | 실행 주체 | 비고 | +|---------|-----------------|----------|------| +| **sam** DB | `api/database/migrations/` | API Jenkinsfile | sam DB 스키마/데이터 변경은 API에서만 | +| **codebridge** DB | `mng/database/migrations/` | MNG Jenkinsfile | codebridge DB 변경은 MNG에서만 | + +### 이력 관리 + +- API와 MNG 모두 `sam.migrations` 테이블에 이력 기록 (Laravel 기본 동작) +- 양쪽에서 `php artisan migrate --force`를 실행해도 **파일명이 겹치지 않으면 충돌 없음** +- Laravel은 자기 폴더의 파일만 스캔하고, `sam.migrations`에 이미 있으면 skip + +``` +sam.migrations 테이블: +1 (API 실행) ✓ ← API migrate 시: 이미 있으니 skip +2 (API 실행) ✓ +3 (MNG 실행) ✓ ← API는 이 파일이 없으므로 모름 (무관) +4 (API 실행) ✓ +5 (MNG 실행) ✓ ← MNG migrate 시: 이미 있으니 skip +``` + +### 주의사항 + +- `--force`는 production 환경에서 확인 프롬프트를 건너뛰는 옵션 (순서 충돌과 무관) +- 파일명(타임스탬프)이 겹치지 않도록 주의 +- sam DB를 변경하는 마이그레이션은 **반드시 API에서** 작성 (MNG에서 sam DB 변경 금지) + +--- + ## [운영] MySQL 접속 ```bash @@ -42,32 +74,82 @@ mysqldump -u hskwon --single-transaction --routines --triggers --all-databases | mysqldump -u hskwon --single-transaction sam 테이블명 > /tmp/sam_테이블명_$(date +%Y%m%d_%H%M%S).sql ``` -### [CI/CD] 자동 백업 (운영 DB) +### [개발] 자동 백업 -CI/CD 서버 crontab에서 매일 03:00에 원격 백업 수행. sam_backup 사용자로 운영 DB에 접속. +개발서버 `/etc/crontab`에서 root로 매일 04:30 실행. -**스크립트:** /home/hskwon/scripts/backup-db.sh -**저장소:** /home/hskwon/backups/mysql/ -**보존:** 14일 +| 항목 | 값 | +|------|-----| +| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` | +| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) | +| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` | +| 대상 DB | sam, sam_stat, codebridge | +| 보존 | daily 14일, weekly 28일 (일요일 자동 복사) | +| 로그 | `/data/backup/mysql/logs/backup.log` | +| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) | ```bash -# 수동 원격 백업 -ssh sam-prod "mysqldump --single-transaction --routines sam" | gzip \ - > /home/hskwon/backups/mysql/sam_production_$(date +%Y%m%d).sql.gz +# 수동 실행 +sudo /home/webservice/api/scripts/backup/sam-db-backup.sh + +# 백업 확인 +ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/ +cat /data/backup/mysql/.backup_status + +# 로그 확인 +tail -20 /data/backup/mysql/logs/backup.log ``` -### [CI/CD] Gitea DB 백업 +### [개발] 수동 백업 ```bash -mysqldump --single-transaction --routines --triggers gitea \ - | gzip > /home/hskwon/backups/mysql/gitea_$(date +%Y%m%d_%H%M%S).sql.gz +# 개별 DB (codebridge 사용자) +mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz +mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz +``` + +### [개발] DB 복구 + +```bash +# 전체 DB 복구 +gunzip -c /data/backup/mysql/daily/2026-03-19/sam_20260319_0430.sql.gz | mysql -ucodebridge -p sam + +# sam_stat 복구 +gunzip -c /data/backup/mysql/daily/2026-03-19/sam_stat_20260319_0430.sql.gz | mysql -ucodebridge -p sam_stat + +# 주간 백업에서 복구 (7일 이전) +ls /data/backup/mysql/weekly/ +gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam +``` + +### [CI/CD] 자동 백업 (운영 DB + Gitea) + +CI/CD 서버 `/etc/crontab`에서 root로 매일 03:00 실행. sam_backup 사용자로 운영 DB에 원격 접속. + +| 항목 | 값 | +|------|-----| +| 스크립트 | `/data/scripts/backup-db.sh` | +| 인증 파일 | `/data/scripts/.sam_backup.cnf` (chmod 600) | +| 저장소 | `/data/backups/mysql/` | +| 실행 사용자 | root (`/etc/crontab`) | +| 대상 DB | gitea (로컬, auth_socket), sam + sam_stat + codebridge (운영 원격) | +| 보존 | 14일 | +| 로그 | `/data/backups/mysql/backup.log` | + +```bash +# 수동 실행 +/data/scripts/backup-db.sh + +# 백업 확인 +ls -lht /data/backups/mysql/ | head -10 +tail -10 /data/backups/mysql/backup.log ``` ### 백업 파일 외부 전송 ```bash # 운영서버 -> CI/CD 서버 -scp /tmp/sam_*.sql.gz sam-cicd:/home/hskwon/backups/mysql/ +scp /tmp/sam_*.sql.gz sam-cicd:/data/backups/mysql/ ``` --- diff --git a/dev/deploys/ops-manual/08-troubleshooting.md b/dev/deploys/ops-manual/08-troubleshooting.md index 37ca169..fb93622 100644 --- a/dev/deploys/ops-manual/08-troubleshooting.md +++ b/dev/deploys/ops-manual/08-troubleshooting.md @@ -393,6 +393,54 @@ cat /home/webservice/mng/shared/storage/logs/laravel.log --- +### MNG 배포 후 storage/logs 권한 500 에러 + +**증상:** MNG 배포 직후 finance/barobill 관련 페이지에서 500 에러. Laravel 로그에 에러 기록 없음. + +**원인:** 배포 시 `php artisan migrate --force`가 배포 사용자(hskwon)로 실행되면서 `storage/logs/laravel-YYYY-MM-DD.log` 파일이 hskwon 소유로 생성됨. 이후 웹 요청에서 www-data가 로그 파일에 쓰기 시도 → `Permission denied` → 500 에러. + +**진단:** + +```bash +# 로그 파일 소유자 확인 (www-data:webservice여야 정상) +ls -la /home/webservice/mng/current/storage/logs/ + +# storage/logs가 심링크인지 디렉토리인지 확인 +stat -c '%F' /home/webservice/mng/current/storage/logs + +# tinker로 에러 재현 +cd /home/webservice/mng/current && sudo -u www-data php artisan tinker --execute="Log::info('test');" +``` + +**긴급 조치:** + +```bash +sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel-*.log +``` + +**근본 해결 (2026-03-19 적용):** + +Jenkinsfile에서 `storage/logs`를 디렉토리(`mkdir`)가 아닌 shared 심링크로 생성하도록 수정: + +```bash +# 변경 전 (문제 발생) +mkdir -p storage/logs && sudo chown -R www-data:webservice storage/logs + +# 변경 후 (심링크 — shared는 www-data 소유이므로 권한 문제 없음) +rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs +``` + +**실제 사례 (2026-03-19):** + +1. `fix: [finance] 더존 3자리→KIS 5자리 계정코드 데이터 마이그레이션` 커밋 배포 +2. `migrate --force` 실행 시 로그 출력 → `laravel-2026-03-19.log`가 hskwon:hskwon으로 생성 +3. 이후 웹 요청에서 www-data가 Log::info() 호출 → Permission denied → 500 +4. Nginx 에러 로그에만 `recv() failed (104: Connection reset by peer)` 기록 +5. `chown www-data:webservice`로 긴급 조치 후 즉시 해소 +6. Jenkinsfile 수정으로 재발 방지 + +--- + ### MNG 전자계약(E-Sign) PDF 서명 합성 오류 **증상:** 전자계약 완료 후 다운로드한 PDF에 서명/도장/텍스트가 적용되지 않음. DB에서 `signed_file_path`가 null. diff --git a/dev/deploys/ops-manual/10-backup-recovery.md b/dev/deploys/ops-manual/10-backup-recovery.md index cd47e4b..1a8ac4d 100644 --- a/dev/deploys/ops-manual/10-backup-recovery.md +++ b/dev/deploys/ops-manual/10-backup-recovery.md @@ -63,24 +63,85 @@ sudo mysql sam < /path/to/sam_테이블명_백업파일.sql --- +## [개발] DB 자동 백업 + +### 개요 + +개발서버(sam-dev)에서 `/etc/crontab`으로 매일 04:30 자동 백업. + +| 항목 | 값 | +|------|-----| +| 스케줄 | **매일 04:30** (`/etc/crontab`, root 실행) | +| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` | +| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) | +| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` | +| 주간 백업 | `/data/backup/mysql/weekly/` (일요일 자동 복사) | +| 대상 DB | sam, sam_stat, codebridge | +| 보존 | daily **14일**, weekly **28일** | +| 로그 | `/data/backup/mysql/logs/backup.log` | +| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) | + +### 백업 대상 + +| DB | 크기 (gzip) | 최소 크기 검증 | 비고 | +|----|------------|---------------|------| +| sam | ~19MB | 1MB | 메인 개발 DB (285 테이블) | +| sam_stat | ~220KB | 100KB | 통계 DB (20 테이블) | +| codebridge | ~5.4MB | 100KB | MNG 내부관리 DB (101 테이블) | + +### 수동 실행 및 확인 + +```bash +# 수동 실행 +sudo /home/webservice/api/scripts/backup/sam-db-backup.sh + +# 백업 확인 +ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/ + +# 상태 확인 (JSON) +cat /data/backup/mysql/.backup_status + +# 로그 확인 +tail -20 /data/backup/mysql/logs/backup.log +``` + +### DB 복구 + +```bash +# sam DB 복구 +gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam + +# sam_stat 복구 +gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_stat_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam_stat + +# codebridge 복구 +gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/codebridge_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p codebridge + +# 주간 백업에서 복구 (7일 이전) +ls /data/backup/mysql/weekly/ +gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam +``` + +--- + ## [CI/CD] Gitea 백업/복구 ### 백업 ```bash # 전체 백업 (저장소 + DB + 설정) -sudo mkdir -p /home/hskwon/backups/gitea +sudo mkdir -p /data/backups/gitea sudo -u git /usr/local/bin/gitea dump \ --config /etc/gitea/app.ini \ --tempdir /tmp \ - --file /home/hskwon/backups/gitea/gitea-dump-$(date +%Y%m%d).zip + --file /data/backups/gitea/gitea-dump-$(date +%Y%m%d).zip # 저장소만 -sudo tar czf /home/hskwon/backups/gitea/repos-$(date +%Y%m%d).tar.gz \ +sudo tar czf /data/backups/gitea/repos-$(date +%Y%m%d).tar.gz \ /var/lib/gitea/data/repositories/ -# DB만 -mysqldump --single-transaction gitea | gzip > /home/hskwon/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz +# DB만 (sudo mysql 사용 — auth_socket) +sudo mysqldump --single-transaction --routines --triggers gitea | gzip > /data/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz ``` ### 복구 @@ -171,30 +232,31 @@ CI/CD 서버(sam-cicd)에서 운영 서버(sam-prod)의 MySQL DB를 원격으로 | 항목 | 값 | |------|-----| -| 스케줄 | **매일 03:00** (crontab) | -| 스크립트 | `/home/hskwon/scripts/backup-db.sh` | -| 인증 정보 | `/home/hskwon/.sam_backup.cnf` (chmod 600) | -| 저장소 | `/home/hskwon/backups/mysql/` | +| 스케줄 | **매일 03:00** (`/etc/crontab`, root 실행) | +| 스크립트 | `/data/scripts/backup-db.sh` (owner: root) | +| 인증 정보 | `/data/scripts/.sam_backup.cnf` (owner: root, chmod 600) | +| 저장소 | `/data/backups/mysql/` | | 보존 기간 | **14일** (자동 삭제) | -| 로그 | `/home/hskwon/backups/mysql/backup.log` | +| 로그 | `/data/backups/mysql/backup.log` | ### 백업 대상 | DB | 서버 | 사용자 | 크기 (gzip) | 비고 | |----|------|--------|------------|------| -| gitea | localhost | root (auth_socket) | ~50KB | Gitea DB | -| sam | 211.117.60.189 (운영) | sam_backup | ~9.3MB | 운영 메인 DB (295 테이블) | -| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB (20 테이블) | +| gitea | localhost | root (auth_socket) | ~508KB | Gitea DB | +| sam | 211.117.60.189 (운영) | sam_backup | ~3.5MB | 운영 메인 DB | +| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB | +| codebridge | 211.117.60.189 (운영) | sam_backup | ~117KB | MNG 내부관리 DB | ### 백업 스크립트 ```bash -# /home/hskwon/scripts/backup-db.sh +# /data/scripts/backup-db.sh #!/bin/bash set -e -BACKUP_DIR="/home/hskwon/backups/mysql" -BACKUP_CNF="/home/hskwon/.sam_backup.cnf" +BACKUP_DIR="/data/backups/mysql" +BACKUP_CNF="/data/scripts/.sam_backup.cnf" DATE=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=14 @@ -207,7 +269,8 @@ mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/ if [ -f "$BACKUP_CNF" ]; then mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam | gzip > $BACKUP_DIR/sam_production_$DATE.sql.gz mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam_stat | gzip > $BACKUP_DIR/sam_stat_production_$DATE.sql.gz - echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat)" >> $BACKUP_DIR/backup.log + mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > $BACKUP_DIR/codebridge_production_$DATE.sql.gz + echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat + codebridge)" >> $BACKUP_DIR/backup.log else echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log fi @@ -219,30 +282,33 @@ find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete ### 인증 설정 ```ini -# /home/hskwon/.sam_backup.cnf (chmod 600) +# /data/scripts/.sam_backup.cnf (chmod 600) [client] user=sam_backup password=<백업용_비밀번호> ``` -### 크론탭 (sam-cicd 서버, hskwon 유저) +### 크론탭 (`/etc/crontab`, root 실행) ```crontab -# SAM DB 백업 (매일 새벽 3시) -0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1 +# SAM DB 백업 (매일 새벽 3시) - root 실행 (계정 독립) +0 3 * * * root /data/scripts/backup-db.sh >> /data/backups/mysql/backup.log 2>&1 ``` +> root로 실행해야 Gitea DB의 auth_socket 인증이 동작한다. +> 특정 사용자 계정에 의존하지 않아 계정 삭제 시에도 영향 없음. + ### 수동 실행 및 확인 ```bash # 수동 백업 실행 -/home/hskwon/scripts/backup-db.sh +/data/scripts/backup-db.sh # 백업 파일 확인 -ls -lht /home/hskwon/backups/mysql/ +ls -lht /data/backups/mysql/ # 백업 로그 확인 -tail -10 /home/hskwon/backups/mysql/backup.log +tail -10 /data/backups/mysql/backup.log # 크론 스케줄 확인 crontab -l @@ -251,11 +317,13 @@ crontab -l ### 백업 복원 (CI/CD → 운영) ```bash -# sam DB 복원 (운영 서버에서 실행) -gunzip -c /path/to/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam +# CI/CD에서 운영서버로 백업 파일 전송 +scp /data/backups/mysql/sam_production_YYYYMMDD_HHMMSS.sql.gz sam-prod:/tmp/ -# sam_stat DB 복원 -gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat +# 운영 서버에서 복원 +gunzip -c /tmp/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam +gunzip -c /tmp/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat +gunzip -c /tmp/codebridge_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p codebridge ``` ### 운영 MySQL 백업 사용자 (운영 서버 설정) diff --git a/dev/dev_plans/[TODO] deploy-account-migration-plan.md b/dev/dev_plans/[TODO] deploy-account-migration-plan.md new file mode 100644 index 0000000..be69f44 --- /dev/null +++ b/dev/dev_plans/[TODO] deploy-account-migration-plan.md @@ -0,0 +1,461 @@ +# deploy 전용 계정 전환 계획 + +> **작성일**: 2026-03-19 +> **상태**: 계획 수립 +> **목적**: 개인 계정(hskwon, pro, kkk) 의존 제거 → 시스템 계정(deploy, root, www-data)으로 전환 +> **대상 서버**: sam-prod, sam-cicd, sam-dev (3대) + +--- + +## 1. 현황 요약 + +### 개인 계정 의존 항목 (20개) + +| 서버 | 항목 | 현재 계정 | 전환 대상 | +|------|------|----------|----------| +| **prod** | PM2 프로세스 + startup 서비스 | hskwon | root | +| **prod** | Jenkinsfile DEPLOY_USER (api, mng, react, sales) | hskwon | deploy | +| **prod** | releases/current 심링크 소유 | hskwon | deploy | +| **prod** | /home/webservice/ 디렉토리 소유 | hskwon | root | +| **prod** | ecosystem.config.js 소유 | hskwon | root | +| **prod** | shared/.env 소유 (api, mng) | hskwon | deploy:webservice | +| **prod** | sales/.env (chmod 600) | hskwon | deploy:webservice (640) | +| **prod** | backups/ 디렉토리 | hskwon | root | +| **cicd** | /data/ 디렉토리 | hskwon:kkk | root:root | +| **cicd** | /data/backups/mysql/ 백업 파일 | hskwon:kkk | root:root | +| **cicd** | hskwon 빈 crontab 잔존 | hskwon | 삭제 | +| **dev** | Laravel scheduler (/etc/crontab) | hskwon | www-data | +| **dev** | Gitea cache 정리 (hskwon crontab) | hskwon | root (/etc/crontab) | +| **dev** | PM2 프로세스 + startup 서비스 | hskwon | root | +| **dev** | /home/webservice/ 디렉토리 소유 | hskwon | root | +| **dev** | sales_org/ 소유 | pro | root (또는 삭제) | +| **dev** | sam_backup_20260317.sql 임시파일 | pro | 삭제 | +| **모든 Jenkinsfile** | DEPLOY_USER = 'hskwon' | hskwon | deploy | +| **mng Jenkinsfile** | storage/logs mkdir (심링크 아님) | — | 심링크로 변경 | +| **Jenkins credential** | deploy-ssh-key → hskwon 키 | hskwon | deploy 키 | + +--- + +## 2. 전환 계획 + +### Phase 0: deploy 계정 생성 (서비스 영향 없음) + +> 기존 hskwon 방식이 그대로 동작하는 상태에서 새 계정만 준비 + +#### 0-1. 3대 서버에 deploy 계정 생성 + +```bash +# sam-prod +sudo useradd -r -m -s /bin/bash -G webservice -c 'CI/CD Deploy Account' deploy +sudo passwd -l deploy # 패스워드 로그인 차단 (SSH 키만 허용) + +# sam-dev +sudo useradd -r -m -s /bin/bash -G develop -c 'CI/CD Deploy Account' deploy +sudo passwd -l deploy + +# sam-cicd (직접 SSH 접속은 없지만 일관성) +sudo useradd -r -m -s /bin/bash -c 'CI/CD Deploy Account' deploy +sudo passwd -l deploy +``` + +#### 0-2. SSH 키 생성 (sam-cicd에서) + +```bash +# Jenkins가 사용할 SSH 키 생성 +sudo -u jenkins ssh-keygen -t ed25519 -f /var/lib/jenkins/.ssh/id_ed25519_deploy -N '' -C 'jenkins-deploy@sam-cicd' +``` + +#### 0-3. SSH 공개키 배포 + +```bash +# sam-prod +sudo mkdir -p /home/deploy/.ssh +sudo cp /var/lib/jenkins/.ssh/id_ed25519_deploy.pub /tmp/deploy_key.pub +# (scp로 전송 후) +sudo sh -c 'cat /tmp/deploy_key.pub >> /home/deploy/.ssh/authorized_keys' +sudo chown -R deploy:deploy /home/deploy/.ssh +sudo chmod 700 /home/deploy/.ssh +sudo chmod 600 /home/deploy/.ssh/authorized_keys + +# sam-dev (동일) +``` + +#### 0-4. sudoers 설정 (sam-prod, sam-dev) + +```bash +# /etc/sudoers.d/deploy +sudo visudo -f /etc/sudoers.d/deploy +``` + +``` +# sam-prod용 +deploy ALL=(ALL) NOPASSWD: /usr/sbin/service php8.4-fpm reload +deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm +deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart php8.4-fpm +deploy ALL=(ALL) NOPASSWD: /usr/bin/supervisorctl restart sam-queue-worker\:* +deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice * +deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 * +deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 * +``` + +``` +# sam-dev용 +deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm +deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice * +deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:develop * +deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 * +deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 * +``` + +#### 0-5. SSH 연결 테스트 + +```bash +# sam-cicd에서 실행 +sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@211.117.60.189 'whoami && hostname' +sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@114.203.209.83 'whoami && hostname' +``` + +#### 0-6. deploy를 webservice 그룹에 추가 확인 + +```bash +# sam-prod +id deploy +# → deploy groups: deploy webservice + +# sam-dev +id deploy +# → deploy groups: deploy develop +``` + +**Phase 0 완료 기준**: deploy 계정으로 SSH + sudo 테스트 통과. 기존 서비스 영향 **없음**. + +--- + +### Phase 1: Jenkins 배포 전환 (배포 중단 5분) + +> **작업 시간**: 야간 또는 사용자 없는 시간 +> **전제**: Phase 0 완료, deploy SSH 연결 확인됨 + +#### 1-1. Jenkins credential 추가 + +Jenkins 웹 UI → Credentials → Global: +- ID: `deploy-ssh-key-v2` (기존 유지한 채 새로 추가) +- 유형: SSH Username with private key +- Username: `deploy` +- Private Key: `/var/lib/jenkins/.ssh/id_ed25519_deploy` + +#### 1-2. Jenkinsfile 수정 (4개 저장소) + +**api/Jenkinsfile:** +```groovy +environment { + DEPLOY_USER = 'deploy' // hskwon → deploy + // ... +} +// sshagent: deploy-ssh-key → deploy-ssh-key-v2 +``` + +**mng/Jenkinsfile:** +```groovy +environment { + DEPLOY_USER = 'deploy' // hskwon → deploy +} +// + storage/logs 심링크 수정 (500 에러 재발 방지) +// 변경 전: mkdir -p ... storage/logs && sudo chown ... +// 변경 후: rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs +``` + +**react/Jenkinsfile, sales/Jenkinsfile:** +```groovy +environment { + DEPLOY_USER = 'deploy' // hskwon → deploy +} +``` + +#### 1-3. Jenkinsfile credential ID 변경 + +모든 Jenkinsfile에서: +```groovy +// 변경 전 +sshagent(credentials: ['deploy-ssh-key']) { + +// 변경 후 +sshagent(credentials: ['deploy-ssh-key-v2']) { +``` + +#### 1-4. 테스트 배포 + +``` +1. mng develop push → 개발서버 배포 확인 +2. mng main push → 운영서버 배포 확인 +3. api main push → Stage → 승인 → Production 확인 +4. react develop push → 개발서버 확인 +``` + +#### 1-5. 파일 소유권 정리 (sam-prod) + +```bash +# /home/webservice 최상위 +sudo chown root:webservice /home/webservice/ +sudo chown root:webservice /home/webservice/ecosystem.config.js + +# shared/.env 소유자 → deploy (Jenkins가 배포 시 접근 가능) +sudo chown deploy:webservice /home/webservice/api/shared/.env +sudo chown deploy:webservice /home/webservice/mng/shared/.env +sudo chmod 640 /home/webservice/api/shared/.env +sudo chmod 640 /home/webservice/mng/shared/.env + +# sales .env (600 → 640) +sudo chown deploy:webservice /home/webservice/sales/.env +sudo chmod 640 /home/webservice/sales/.env + +# api, mng, react 디렉토리 (releases/shared 상위) +for d in api api-stage mng react react-stage sales landing; do + sudo chown deploy:webservice /home/webservice/$d 2>/dev/null +done + +# backups +sudo chown root:webservice /home/webservice/backups/ +``` + +**Phase 1 완료 기준**: Jenkins → deploy 계정으로 4개 프로젝트 배포 성공. hskwon SSH 키 미사용. + +--- + +### Phase 2: PM2 전환 (Next.js 다운타임 1~2분) + +> **작업 시간**: 야간 필수 (sam.it.kr 일시 중단) + +#### 2-1. sam-prod PM2 전환 + +```bash +# 1. 현재 PM2 정지 +pm2 stop all +pm2 kill + +# 2. 기존 PM2 서비스 비활성화 +sudo systemctl stop pm2-hskwon +sudo systemctl disable pm2-hskwon + +# 3. root PM2 서비스 생성 +sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root +# → /etc/systemd/system/pm2-root.service 생성 + +# 4. ecosystem.config.js로 시작 +cd /home/webservice && sudo pm2 start ecosystem.config.js +sudo pm2 save + +# 5. 서비스 확인 +sudo pm2 status +curl -sI https://sam.it.kr | head -3 +curl -sI https://stage.sam.it.kr | head -3 + +# 6. 이전 서비스 파일 삭제 +sudo rm /etc/systemd/system/pm2-hskwon.service +sudo systemctl daemon-reload +``` + +#### 2-2. sam-dev PM2 전환 + +```bash +# 동일 절차 +pm2 stop all && pm2 kill +sudo systemctl stop pm2-hskwon && sudo systemctl disable pm2-hskwon +sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root +cd /home/webservice && sudo pm2 start ecosystem.config.js +sudo pm2 save +sudo rm /etc/systemd/system/pm2-hskwon.service +sudo systemctl daemon-reload + +# 확인 +sudo pm2 status +``` + +**Phase 2 완료 기준**: PM2가 root로 실행. `pm2 status`에서 user=root 확인. 부팅 후 자동 복구 테스트 (선택). + +--- + +### Phase 3: Cron/시스템 정리 (서비스 영향 없음) + +#### 3-1. sam-dev: Laravel scheduler 전환 + +```bash +# /etc/crontab 수정 +# 변경 전: +# * * * * * hskwon cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1 +# 변경 후: +* * * * * www-data cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1 +``` + +#### 3-2. sam-dev: Gitea cache 정리 → root crontab + +```bash +# hskwon crontab에서 삭제 +crontab -r # (이미 빈 crontab) + +# /etc/crontab에 추가 +# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type f -mtime +7 -delete 2>/dev/null +# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type d -empty -delete 2>/dev/null +``` + +#### 3-3. sam-cicd: 빈 crontab 삭제 + +```bash +crontab -r # hskwon 빈 crontab 삭제 +``` + +#### 3-4. sam-cicd: /data/ 소유권 변경 + +```bash +sudo chown -R root:root /data/ +sudo chown root:root /data/scripts/backup-db.sh +sudo chown root:root /data/scripts/.sam_backup.cnf +sudo chmod 600 /data/scripts/.sam_backup.cnf +``` + +#### 3-5. sam-dev: 디렉토리 소유권 정리 + +```bash +# /home/webservice 최상위 +sudo chown root:develop /home/webservice/ + +# 임시 파일 삭제 +sudo rm -f /home/webservice/sam_backup_20260317.sql +sudo rm -f /home/webservice/demo.tar.gz # 필요 여부 확인 + +# sales_org → 필요 여부 확인 후 삭제 또는 소유권 변경 +sudo chown -R root:develop /home/webservice/sales_org/ +``` + +**Phase 3 완료 기준**: 개인 계정 crontab 전부 비어 있음. /data/, /home/webservice/ root 소유. + +--- + +### Phase 4: 검증 및 문서 업데이트 + +#### 4-1. 전체 서버 개인 계정 의존 재점검 + +```bash +# 3대 서버에서 실행 +for u in hskwon pro kkk; do + echo "=== $u crontab ===" + sudo crontab -u $u -l 2>/dev/null || echo 'none' +done + +# PM2 소유자 확인 +pm2 status # user 컬럼 = root + +# 서비스 상태 +sudo systemctl status pm2-root +sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor +``` + +#### 4-2. ops-manual 문서 업데이트 + +- `01-server-overview.md`: 사용자 목록에 deploy 추가, 디렉토리 소유권 +- `05-deployment.md`: Jenkinsfile DEPLOY_USER=deploy 반영 +- `09-security.md`: deploy 계정 설명, sudoers 설정 +- `10-backup-recovery.md`: crontab 실행 주체 확인 + +#### 4-3. 이 계획 문서 → 완료 후 삭제 + +--- + +## 3. 롤백 계획 + +| Phase | 롤백 방법 | 소요 시간 | +|-------|----------|----------| +| Phase 0 | 계정 삭제 (`userdel deploy`) | 1분 | +| Phase 1 | Jenkinsfile DEPLOY_USER → hskwon 복원, credential 원복 | 5분 | +| Phase 2 | `sudo pm2 kill` → hskwon으로 PM2 재시작 | 2분 | +| Phase 3 | crontab, 소유권 원복 | 5분 | + +**핵심**: Phase 1까지는 기존 hskwon 방식이 병행 가능하므로 즉시 롤백 가능. + +--- + +## 4. 작업 일정 (제안) + +| Phase | 작업 | 시간대 | 서비스 영향 | +|-------|------|--------|-----------| +| **0** | deploy 계정 생성 + SSH 키 + sudoers | 업무 시간 | 없음 | +| **1** | Jenkinsfile 수정 + 테스트 배포 | 업무 시간 (배포 조율) | 배포 불가 5분 | +| **2** | PM2 전환 | **야간** | Next.js 다운 1~2분 | +| **3** | Cron/소유권 정리 | 업무 시간 | 없음 | +| **4** | 검증 + 문서 | 업무 시간 | 없음 | + +--- + +## 5. 함께 수정하는 항목 (Jenkinsfile 수정 시) + +### MNG storage/logs 심링크 수정 (500 에러 재발 방지) + +현재 Jenkinsfile: +```bash +mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs && +sudo chown -R www-data:webservice storage/logs && +``` + +수정 후: +```bash +mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} && +rm -rf storage/logs && +ln -sfn /home/webservice/mng/shared/storage/logs storage/logs && +``` + +> `migrate --force` 실행 시 deploy 계정으로 로그 파일이 생성되어도, shared/storage/logs는 +> www-data:webservice 소유이므로 권한 문제 없음. + +### API storage 권한도 동일 패턴 적용 + +현재 API Jenkinsfile에서 `sudo chown -R www-data:webservice storage bootstrap/cache`로 해결하고 있으나, +storage/logs도 shared 심링크로 통일하는 것이 더 안전. + +--- + +## 6. 체크리스트 + +### Phase 0 +- [ ] sam-prod에 deploy 계정 생성 + webservice 그룹 +- [ ] sam-dev에 deploy 계정 생성 + develop 그룹 +- [ ] sam-cicd에 deploy 계정 생성 +- [ ] SSH 키 생성 (sam-cicd jenkins 사용자) +- [ ] SSH 공개키 → sam-prod, sam-dev authorized_keys +- [ ] sudoers 설정 (sam-prod, sam-dev) +- [ ] SSH 연결 테스트 통과 + +### Phase 1 +- [ ] Jenkins credential 추가 (deploy-ssh-key-v2) +- [ ] api/Jenkinsfile DEPLOY_USER + credential 변경 +- [ ] mng/Jenkinsfile DEPLOY_USER + credential + storage/logs 심링크 수정 +- [ ] react/Jenkinsfile DEPLOY_USER + credential 변경 +- [ ] sales/Jenkinsfile DEPLOY_USER + credential 변경 +- [ ] mng 테스트 배포 (develop → 개발서버) +- [ ] mng 테스트 배포 (main → 운영서버) +- [ ] api 테스트 배포 (main → Stage → Production) +- [ ] react 테스트 배포 (develop → 개발서버) +- [ ] sam-prod 파일 소유권 정리 +- [ ] .env 권한 640 확인 (api, mng, sales) + +### Phase 2 +- [ ] sam-prod PM2 → root 전환 (야간) +- [ ] sam.it.kr 접속 확인 +- [ ] stage.sam.it.kr 접속 확인 +- [ ] sam-dev PM2 → root 전환 +- [ ] dev.codebridge-x.com 접속 확인 + +### Phase 3 +- [ ] sam-dev scheduler → www-data +- [ ] sam-dev Gitea cache 정리 → root /etc/crontab +- [ ] sam-cicd hskwon 빈 crontab 삭제 +- [ ] sam-cicd /data/ 소유권 → root +- [ ] sam-dev 임시 파일 삭제 + 소유권 정리 + +### Phase 4 +- [ ] 전체 서버 개인 계정 의존 재점검 (0건 확인) +- [ ] ops-manual 문서 업데이트 +- [ ] 이 계획 문서 삭제 + +--- + +**최종 업데이트**: 2026-03-19