refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동) - 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/) - 기획팀 폴더 requests/ 생성 - plans/ → dev/dev_plans/ 이름 변경 - README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용) - resources.md 신규 (노션 링크용, assets/brochure 이관 예정) - CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동 - 전체 참조 경로 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
343
dev/deploys/ops-manual/01-server-overview.md
Normal file
343
dev/deploys/ops-manual/01-server-overview.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 1. 서버 인프라 개요
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## 운영서버 (sam-prod)
|
||||
|
||||
### 서버 사양
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| IP | 211.117.60.189 |
|
||||
| 호스트명 | sam-prod |
|
||||
| OS | Ubuntu 24.04.4 LTS |
|
||||
| 커널 | 6.8.0-100-generic |
|
||||
| CPU | 2 vCPU |
|
||||
| RAM | 8GB |
|
||||
| Swap | 4GB |
|
||||
| 디스크 | 98GB (여유 79GB) |
|
||||
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
|
||||
|
||||
### 도메인 목록
|
||||
|
||||
| 도메인 | 서비스 | 백엔드 | 포트 |
|
||||
|--------|--------|--------|------|
|
||||
| sam.it.kr | Next.js 15 프론트엔드 | PM2 cluster x2 | 3000 |
|
||||
| api.sam.it.kr | Laravel 12 API | PHP-FPM api pool | unix socket |
|
||||
| mng.codebridge-x.com | Laravel 12 Admin | PHP-FPM admin pool | unix socket |
|
||||
| sales.codebridge-x.com | Plain PHP 레거시 | PHP-FPM sales pool | unix socket |
|
||||
| codebridge-x.com (+ www) | 정적 랜딩페이지 | Nginx direct | 80/443 |
|
||||
| stage.sam.it.kr | Next.js Stage | PM2 fork x1 | 3100 |
|
||||
| stage-api.sam.it.kr | Laravel API Stage | PHP-FPM api-stage pool | unix socket |
|
||||
|
||||
모든 도메인은 Let's Encrypt SSL 적용 (알림: develop@codebridge-x.com).
|
||||
|
||||
### 서비스 현황
|
||||
|
||||
| 서비스 | 버전 | 포트 | 상태 |
|
||||
|--------|------|------|------|
|
||||
| Nginx | 1.24.0 | 80/443 | active |
|
||||
| PHP-FPM | 8.4.18 | unix socket (4개 pool) | active |
|
||||
| MySQL | 8.4.8 | 3306 | active |
|
||||
| Redis | 7.0.15 | 6379 (localhost) | active |
|
||||
| PM2 | 6.0.14 | 3000 (cluster x2), 3100 (fork x1) | active |
|
||||
| Supervisor | - | - | active (queue worker x2) |
|
||||
| node_exporter | 1.8.2 | 9100 | active |
|
||||
| Certbot | 2.9.0 | - | timer active |
|
||||
| fail2ban | - | - | active |
|
||||
|
||||
### 주요 디렉토리
|
||||
|
||||
```
|
||||
/home/webservice/
|
||||
api/ Laravel API (운영) - releases/shared 구조
|
||||
current -> releases/...
|
||||
releases/
|
||||
shared/ (.env, storage/)
|
||||
api-stage/ Laravel API (Stage) - 동일 구조
|
||||
mng/ Laravel Admin - 동일 구조
|
||||
sales/ Plain PHP 레거시 (.env, uploads/)
|
||||
react/ Next.js 운영 - releases/shared 구조
|
||||
react-stage/ Next.js Stage - 동일 구조
|
||||
landing/ 정적 랜딩페이지
|
||||
ecosystem.config.js PM2 설정
|
||||
```
|
||||
|
||||
### 주요 설정 파일
|
||||
|
||||
| 구분 | 경로 |
|
||||
|------|------|
|
||||
| Nginx 메인 설정 | /etc/nginx/nginx.conf |
|
||||
| Nginx 사이트 설정 | /etc/nginx/sites-available/*.conf |
|
||||
| Nginx 보안 스니펫 | /etc/nginx/snippets/security.conf |
|
||||
| PHP-FPM Pool (API) | /etc/php/8.4/fpm/pool.d/api.conf |
|
||||
| PHP-FPM Pool (Admin) | /etc/php/8.4/fpm/pool.d/admin.conf |
|
||||
| PHP-FPM Pool (Sales) | /etc/php/8.4/fpm/pool.d/sales.conf |
|
||||
| PHP-FPM Pool (API Stage) | /etc/php/8.4/fpm/pool.d/api-stage.conf |
|
||||
| MySQL 튜닝 | /etc/mysql/mysql.conf.d/sam-tuning.cnf |
|
||||
| Redis | /etc/redis/redis.conf |
|
||||
| Supervisor | /etc/supervisor/conf.d/sam-queue.conf |
|
||||
| PM2 | /home/webservice/ecosystem.config.js |
|
||||
| API .env | /home/webservice/api/shared/.env |
|
||||
| MNG .env | /home/webservice/mng/shared/.env |
|
||||
| Sales .env | /home/webservice/sales/.env |
|
||||
|
||||
### 메모리 배분
|
||||
|
||||
| 서비스 | 할당 | 설정 |
|
||||
|--------|------|------|
|
||||
| MySQL 8.4 | ~2GB | innodb_buffer_pool_size=2G |
|
||||
| Redis | ~0.5GB | maxmemory 512mb |
|
||||
| PHP-FPM (API) | ~0.8GB | max_children=10 |
|
||||
| PHP-FPM (Admin) | ~0.3GB | max_children=5 |
|
||||
| PHP-FPM (Sales) | ~0.2GB | max_children=3 |
|
||||
| PHP-FPM (API-Stage) | ~0.2GB | max_children=3 |
|
||||
| Next.js 운영 (PM2 cluster×2) | ~0.6GB | max-old-space-size=256 |
|
||||
| Next.js Stage (PM2 fork×1) | ~0.15GB | max-old-space-size=128 |
|
||||
| Supervisor (Queue Worker) | ~0.1GB | numprocs=2 |
|
||||
| Nginx | ~0.1GB | worker_connections 1024 |
|
||||
| node_exporter | ~10MB | - |
|
||||
| OS + 여유 | ~2.9GB | 스왑 4GB |
|
||||
|
||||
### 방화벽 (UFW) 규칙
|
||||
|
||||
| 포트 | 프로토콜 | 허용 범위 | 용도 |
|
||||
|------|----------|-----------|------|
|
||||
| 22 | TCP | Anywhere | SSH |
|
||||
| 80 | TCP | Anywhere | HTTP |
|
||||
| 443 | TCP | Anywhere | HTTPS |
|
||||
| 9100 | TCP | 110.10.147.46 only | node_exporter (Prometheus) |
|
||||
| 3306 | TCP | 110.10.147.46 only | MySQL 백업 (CI/CD 서버) |
|
||||
|
||||
### 데이터베이스 사용자
|
||||
|
||||
| 사용자 | 인증 방식 | 권한 | 용도 |
|
||||
|--------|-----------|------|------|
|
||||
| codebridge@localhost | 비밀번호 | sam, sam_stage, sam_stat, codebridge | 애플리케이션 |
|
||||
| hskwon@localhost | auth_socket | ALL (WITH GRANT OPTION) | 관리자 |
|
||||
| root@localhost | auth_socket | ALL | 시스템 (sudo mysql) |
|
||||
| sam_backup@110.10.147.46 | 비밀번호 | SELECT, LOCK TABLES (sam, sam_stat) | CI/CD 백업 |
|
||||
|
||||
---
|
||||
|
||||
## CI/CD 서버 (sam-cicd)
|
||||
|
||||
### 서버 사양
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 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) |
|
||||
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
|
||||
|
||||
### 도메인 매핑
|
||||
|
||||
| 도메인 | 서비스 | 백엔드 포트 | SSL |
|
||||
|--------|--------|------------|-----|
|
||||
| git.sam.it.kr | Gitea | :3000 | Let's Encrypt |
|
||||
| ci.sam.it.kr | Jenkins | :8080 | Let's Encrypt |
|
||||
| monitor.sam.it.kr | Grafana | :3100 | Let's Encrypt |
|
||||
|
||||
### 서비스 현황
|
||||
|
||||
| 서비스 | 버전 | 포트 | 도메인 |
|
||||
|--------|------|------|--------|
|
||||
| Nginx | 1.24.0 | 80/443 | 리버스 프록시 |
|
||||
| Jenkins | LTS (2.541.2) | 8080 | ci.sam.it.kr |
|
||||
| Gitea | 1.22.6 | 3000 | git.sam.it.kr |
|
||||
| MySQL | 8.4.8 | 3306 | - |
|
||||
| Prometheus | 2.51.0 | 9090 | - (localhost only) |
|
||||
| Grafana | - | 3100 | monitor.sam.it.kr |
|
||||
| node_exporter | 1.8.2 | 9100 | - |
|
||||
| Java | OpenJDK 21.0.10 | - | Jenkins 런타임 |
|
||||
| Certbot | - | - | SSL 자동 갱신 |
|
||||
| fail2ban | - | - | SSH 보호 |
|
||||
|
||||
### 메모리 배분
|
||||
|
||||
| 서비스 | 할당 | 설정 |
|
||||
|--------|------|------|
|
||||
| Jenkins | ~2.0GB | -Xmx2048m |
|
||||
| MySQL | ~1.5GB | innodb_buffer_pool_size=1536M |
|
||||
| Gitea | ~0.5GB | Go 기반 |
|
||||
| Prometheus | ~0.5GB | retention 30d |
|
||||
| Grafana | ~0.3GB | - |
|
||||
| Nginx | ~0.1GB | - |
|
||||
| node_exporter | ~10MB | - |
|
||||
| OS + 여유 | ~3.1GB | Swap 4GB |
|
||||
|
||||
### 주요 설정 파일
|
||||
|
||||
| 설정 | 경로 |
|
||||
|------|------|
|
||||
| Nginx 사이트 | /etc/nginx/sites-available/{ci,git,monitor}.sam.it.kr |
|
||||
| Jenkins 홈 | /var/lib/jenkins/ |
|
||||
| Jenkins JVM 설정 | /etc/systemd/system/jenkins.service.d/override.conf |
|
||||
| Jenkins Agent | /var/lib/jenkins-agent/ (workspace, agent.jar) |
|
||||
| Jenkins Agent 서비스 | /etc/systemd/system/jenkins-agent.service |
|
||||
| Jenkins 환경파일 | /var/lib/jenkins/env-files/react/.env.{develop,stage,main} |
|
||||
| Gitea 설정 | /etc/gitea/app.ini |
|
||||
| Gitea 저장소 | /var/lib/gitea/data/repositories/ |
|
||||
| Gitea 로그 | /var/lib/gitea/log/ |
|
||||
| Prometheus 설정 | /etc/prometheus/prometheus.yml |
|
||||
| Prometheus 데이터 | /var/lib/prometheus/ |
|
||||
| Grafana 설정 | /etc/grafana/grafana.ini |
|
||||
| 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/ |
|
||||
|
||||
### 방화벽 (UFW) 규칙
|
||||
|
||||
| 포트 | 프로토콜 | 용도 |
|
||||
|------|---------|------|
|
||||
| 22/tcp | ALLOW | SSH |
|
||||
| 80/tcp | ALLOW | HTTP |
|
||||
| 443/tcp | ALLOW | HTTPS |
|
||||
|
||||
---
|
||||
|
||||
## 개발서버 (sam-dev)
|
||||
|
||||
### 서버 사양
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| IP | 114.203.209.83 |
|
||||
| 호스트명 | sam-dev |
|
||||
| OS | Ubuntu 24.04.2 LTS |
|
||||
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
|
||||
|
||||
### 서비스 현황
|
||||
|
||||
| 서비스 | 포트 | 상태 |
|
||||
|--------|------|------|
|
||||
| Nginx | 80/443 | active |
|
||||
| Apache | 8080 | active (레거시) |
|
||||
| MySQL 8.4 | 3306 (localhost) | active |
|
||||
| Gitea | 3000 | active |
|
||||
| Next.js (PM2) | 3001 | active |
|
||||
| fail2ban | - | active |
|
||||
|
||||
### 방화벽 (UFW) 규칙
|
||||
|
||||
| 포트 | 프로토콜 | 용도 |
|
||||
|------|---------|------|
|
||||
| 22/tcp | ALLOW | SSH |
|
||||
| 80/tcp | ALLOW | HTTP |
|
||||
| 443/tcp | ALLOW | HTTPS |
|
||||
| 3000/tcp | ALLOW | Gitea |
|
||||
|
||||
> MySQL(3306), Apache(8080), Next.js(3001), CUPS(631) 등은 외부 차단
|
||||
|
||||
### 주요 디렉토리
|
||||
|
||||
```
|
||||
/home/webservice/
|
||||
react/ Next.js 프론트엔드
|
||||
api/ Laravel API
|
||||
mng/ Laravel Admin
|
||||
sales/ Plain PHP 레거시
|
||||
|
||||
/data/GIT/samproject/ Gitea bare repositories
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 아키텍처 다이어그램
|
||||
|
||||
### 운영서버
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 운영서버 (2 vCPU / 8GB) │
|
||||
│ Ubuntu 24.04 / IP: 211.117.60.189 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
|
||||
│ │ Nginx │ │ Certbot │ │ UFW (22,80,443,9100) │ │
|
||||
│ └────┬─────┘ └───────────┘ └───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────┴───────────────────────────────────────────────┐ │
|
||||
│ │ sam.it.kr ──────────→ Next.js (PM2 cluster, :3000)│ │
|
||||
│ │ api.sam.it.kr ──────→ PHP-FPM (api pool) │ │
|
||||
│ │ mng.codebridge-x.com ──→ PHP-FPM (admin pool) │ │
|
||||
│ │ sales.codebridge-x.com → PHP-FPM (sales pool) │ │
|
||||
│ │ stage.sam.it.kr ────→ Next.js (PM2 fork, :3100) │ │
|
||||
│ │ stage-api.sam.it.kr → PHP-FPM (api-stage pool) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌─────────────────┐ │
|
||||
│ │ MySQL 8.4 │ │ Redis │ │ Supervisor │ │
|
||||
│ │ (Master) │ │ (캐시/큐) │ │ (Queue Worker) │ │
|
||||
│ └────────────┘ └────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ node_exporter (:9100) → CI/CD Prometheus │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### CI/CD 서버
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ CI/CD서버 (2 vCPU / 8GB) │
|
||||
│ Ubuntu 24.04 / IP: 110.10.147.46 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
|
||||
│ │ Nginx │ │ Certbot │ │ UFW (22,80,443) │ │
|
||||
│ └────┬─────┘ └───────────┘ └───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────┴───────────────────────────────────────────────┐ │
|
||||
│ │ git.sam.it.kr ──────────→ Gitea (:3000) │ │
|
||||
│ │ ci.sam.it.kr ───────────→ Jenkins (:8080) │ │
|
||||
│ │ monitor.sam.it.kr ──────→ Grafana (:3100) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
|
||||
│ │ Gitea │ │ Jenkins │ │ MySQL 8.4 │ │
|
||||
│ │ (운영 Git) │ │ (CI/CD) │ │ (Gitea DB + 백업) │ │
|
||||
│ └────────────┘ └────────────┘ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Prometheus │ │ Grafana │ │
|
||||
│ │ (:9090) │ │ (:3100) │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 도메인 환경 분리
|
||||
|
||||
| 서비스 | 운영 | Stage | 개발 |
|
||||
|--------|------|-------|------|
|
||||
| Front | sam.it.kr | stage.sam.it.kr | dev.codebridge-x.com |
|
||||
| API | api.sam.it.kr | stage-api.sam.it.kr | api.codebridge-x.com |
|
||||
| Admin | mng.codebridge-x.com | - | admin.codebridge-x.com |
|
||||
| Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com |
|
||||
| Landing | codebridge-x.com | - | - |
|
||||
|
||||
### 타이틀 접두사 (환경 구분)
|
||||
|
||||
브라우저 탭에서 환경을 즉시 구분할 수 있도록 타이틀에 접두사를 표시한다.
|
||||
|
||||
| 환경 | 접두사 | 예시 |
|
||||
|------|--------|------|
|
||||
| 로컬 | `[L]` | `[L]SAM_MNG` |
|
||||
| 개발 | `[D]` | `[D]SAM_SYSTEM` |
|
||||
| 운영 | 없음 | `SAM_SYSTEM` |
|
||||
|
||||
**설정 위치:**
|
||||
|
||||
| 프로젝트 | 방식 | 설정 파일 |
|
||||
|---------|------|----------|
|
||||
| mng | `.env`의 `APP_NAME`에 접두사 포함 | 로컬: `mng/.env`, 개발: `/home/webservice/mng/.env` |
|
||||
| api | `.env`의 `APP_NAME`에 접두사 포함 | 로컬: `api/.env`, 개발: `/home/webservice/api/.env` |
|
||||
| react | 코드에서 `NEXT_PUBLIC_APP_ENV` 값으로 자동 판별 | CI/CD: `/var/lib/jenkins/env-files/react/.env.develop` |
|
||||
253
dev/deploys/ops-manual/02-daily-operations.md
Normal file
253
dev/deploys/ops-manual/02-daily-operations.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 2. 일상 운영
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## [운영] 전체 서비스 상태 확인
|
||||
|
||||
```bash
|
||||
# 핵심 서비스 상태 한번에 확인
|
||||
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter
|
||||
|
||||
# PM2 프로세스 상태
|
||||
pm2 status
|
||||
|
||||
# 열린 포트 확인
|
||||
sudo ss -tlnp
|
||||
```
|
||||
|
||||
## [CI/CD] 전체 서비스 상태 확인
|
||||
|
||||
```bash
|
||||
# 모든 핵심 서비스 상태 한 번에 확인
|
||||
sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter
|
||||
|
||||
# 개별 서비스 상태
|
||||
sudo systemctl status jenkins
|
||||
sudo systemctl status gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [운영] .env 파일 편집 시 주의사항
|
||||
|
||||
> **경고:** `vi`로 `.env`를 편집하면 권한이 `600`으로 변경되어 서비스 장애가 발생할 수 있습니다.
|
||||
|
||||
```bash
|
||||
# 편집 전 권한 확인
|
||||
ls -la /home/webservice/api/shared/.env # 640(-rw-r-----)이어야 함
|
||||
|
||||
# 편집 후 반드시 권한 확인 및 복원
|
||||
chmod 640 /home/webservice/api/shared/.env
|
||||
chmod 640 /home/webservice/mng/shared/.env
|
||||
```
|
||||
|
||||
이를 방지하려면 `~/.vimrc`에 `set backupcopy=yes`가 설정되어 있어야 합니다.
|
||||
자세한 내용: [09-security.md - .env 파일 보안](./09-security.md)
|
||||
|
||||
---
|
||||
|
||||
## 시스템 리소스 모니터링
|
||||
|
||||
양쪽 서버 공통 명령어:
|
||||
|
||||
```bash
|
||||
# 메모리 사용량
|
||||
free -h
|
||||
|
||||
# 디스크 사용량
|
||||
df -h
|
||||
|
||||
# CPU 및 프로세스 (실시간)
|
||||
htop
|
||||
|
||||
# 로드 평균 (즉시 확인)
|
||||
uptime
|
||||
|
||||
# 스왑 사용량
|
||||
swapon --show
|
||||
|
||||
# 열린 포트 확인
|
||||
sudo ss -tlnp
|
||||
|
||||
# 프로세스별 메모리 사용량 (상위 10개)
|
||||
ps aux --sort=-%mem | head -11
|
||||
```
|
||||
|
||||
**[CI/CD] 디스크 사용량 상세:**
|
||||
|
||||
```bash
|
||||
sudo du -sh /var/lib/jenkins /var/lib/gitea /var/lib/prometheus /var/lib/mysql /var/log 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 로그 확인
|
||||
|
||||
### [운영] Nginx
|
||||
|
||||
```bash
|
||||
# 접근 로그 (실시간)
|
||||
sudo tail -f /var/log/nginx/api.sam.it.kr.access.log
|
||||
sudo tail -f /var/log/nginx/sam.it.kr.access.log
|
||||
sudo tail -f /var/log/nginx/mng.codebridge-x.com.access.log
|
||||
|
||||
# 에러 로그 (실시간)
|
||||
sudo tail -f /var/log/nginx/api.sam.it.kr.error.log
|
||||
sudo tail -f /var/log/nginx/sam.it.kr.error.log
|
||||
|
||||
# 최근 에러 50줄
|
||||
sudo tail -50 /var/log/nginx/api.sam.it.kr.error.log
|
||||
```
|
||||
|
||||
### [운영] PHP-FPM
|
||||
|
||||
```bash
|
||||
sudo tail -f /var/log/php8.4-fpm.log
|
||||
```
|
||||
|
||||
### [운영] Laravel
|
||||
|
||||
```bash
|
||||
# API 로그
|
||||
sudo tail -f /home/webservice/api/shared/storage/logs/laravel.log
|
||||
|
||||
# Admin(MNG) 로그 — storage/logs가 shared 심링크가 아니므로 current 경로 사용
|
||||
sudo tail -f /home/webservice/mng/current/storage/logs/laravel.log
|
||||
|
||||
# API Stage 로그
|
||||
sudo tail -f /home/webservice/api-stage/shared/storage/logs/laravel.log
|
||||
|
||||
# Queue Worker 로그
|
||||
sudo tail -f /home/webservice/api/shared/storage/logs/queue-worker.log
|
||||
```
|
||||
|
||||
### [운영] PM2 (Next.js)
|
||||
|
||||
```bash
|
||||
# 운영 로그
|
||||
pm2 logs sam-front --lines 50
|
||||
|
||||
# Stage 로그
|
||||
pm2 logs sam-front-stage --lines 50
|
||||
|
||||
# 에러 로그만
|
||||
pm2 logs sam-front --err --lines 50
|
||||
```
|
||||
|
||||
### [운영] Supervisor
|
||||
|
||||
```bash
|
||||
sudo supervisorctl status
|
||||
sudo tail -f /home/webservice/api/shared/storage/logs/queue-worker.log
|
||||
```
|
||||
|
||||
### [운영] MySQL
|
||||
|
||||
```bash
|
||||
sudo tail -f /var/log/mysql/slow.log
|
||||
sudo tail -f /var/log/mysql/error.log
|
||||
```
|
||||
|
||||
### [CI/CD] Jenkins
|
||||
|
||||
```bash
|
||||
sudo journalctl -u jenkins -f
|
||||
sudo journalctl -u jenkins --since "1 hour ago"
|
||||
```
|
||||
|
||||
### [CI/CD] Gitea
|
||||
|
||||
```bash
|
||||
sudo journalctl -u gitea -f
|
||||
sudo tail -f /var/lib/gitea/log/gitea.log
|
||||
```
|
||||
|
||||
### [CI/CD] Prometheus / Grafana
|
||||
|
||||
```bash
|
||||
sudo journalctl -u prometheus -f
|
||||
sudo journalctl -u grafana-server -f
|
||||
```
|
||||
|
||||
### [CI/CD] Nginx / MySQL
|
||||
|
||||
```bash
|
||||
sudo tail -f /var/log/nginx/access.log
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
sudo tail -f /var/log/mysql/error.log
|
||||
```
|
||||
|
||||
### 시스템 로그 (공통)
|
||||
|
||||
```bash
|
||||
# 시스템 전체 로그 (최근)
|
||||
sudo journalctl -xe --no-pager | tail -50
|
||||
|
||||
# 특정 서비스 로그
|
||||
sudo journalctl -u 서비스명 --since "1 hour ago"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSL 인증서 확인 (공통)
|
||||
|
||||
```bash
|
||||
# 전체 인증서 목록 및 만료일
|
||||
sudo certbot certificates
|
||||
|
||||
# 자동 갱신 타이머 상태
|
||||
sudo systemctl status certbot.timer
|
||||
|
||||
# 갱신 테스트 (실제 갱신하지 않음)
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] 네트워크 연결 확인
|
||||
|
||||
```bash
|
||||
# 운영서버 연결
|
||||
ping -c 3 211.117.60.189
|
||||
ssh sam-prod "echo 'prod OK'"
|
||||
|
||||
# 개발서버 연결
|
||||
ping -c 3 114.203.209.83
|
||||
ssh sam-dev "echo 'dev OK'"
|
||||
|
||||
# 웹 서비스 응답 확인
|
||||
curl -sI https://ci.sam.it.kr | head -5
|
||||
curl -sI https://git.sam.it.kr | head -5
|
||||
curl -sI https://monitor.sam.it.kr | head -5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 일일 점검 스크립트
|
||||
|
||||
### [운영]
|
||||
|
||||
```bash
|
||||
echo "=== 서비스 ===" && \
|
||||
for s in nginx php8.4-fpm mysql redis-server supervisor node_exporter; do
|
||||
printf "%-20s %s\n" "$s" "$(systemctl is-active $s)"
|
||||
done && \
|
||||
echo "=== PM2 ===" && pm2 status && \
|
||||
echo "=== 메모리 ===" && free -h | grep Mem && \
|
||||
echo "=== 디스크 ===" && df -h / | tail -1 && \
|
||||
echo "=== SSL ===" && sudo certbot certificates 2>/dev/null | grep "Expiry Date"
|
||||
```
|
||||
|
||||
### [CI/CD]
|
||||
|
||||
```bash
|
||||
echo "=== 서비스 ===" && \
|
||||
for s in nginx jenkins gitea mysql prometheus grafana-server node_exporter; do
|
||||
printf "%-20s %s\n" "$s" "$(systemctl is-active $s)"
|
||||
done && \
|
||||
echo "=== 메모리 ===" && free -h | grep Mem && \
|
||||
echo "=== 디스크 ===" && df -h / | tail -1 && \
|
||||
echo "=== SSL ===" && sudo certbot certificates 2>/dev/null | grep "Expiry Date"
|
||||
```
|
||||
381
dev/deploys/ops-manual/03-service-prod.md
Normal file
381
dev/deploys/ops-manual/03-service-prod.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# 3. 운영서버 서비스 관리
|
||||
|
||||
[목차로 돌아가기](./README.md) | 서버: sam-prod (211.117.60.189)
|
||||
|
||||
---
|
||||
|
||||
## Nginx
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status nginx
|
||||
sudo nginx -t # 설정 테스트 (반드시 reload/restart 전에 실행)
|
||||
sudo systemctl reload nginx # 설정 리로드 (무중단)
|
||||
sudo systemctl restart nginx # 재시작 (연결 끊김 발생)
|
||||
sudo systemctl stop nginx
|
||||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /etc/nginx/nginx.conf | 메인 설정 (worker_connections 1024, client_max_body_size 50M) |
|
||||
| /etc/nginx/sites-available/ | 사이트별 설정 |
|
||||
| /etc/nginx/sites-enabled/ | 활성화된 사이트 (심링크) |
|
||||
| /etc/nginx/snippets/security.conf | 보안 규칙 (.env, .git 차단) |
|
||||
|
||||
**로그 파일:**
|
||||
|
||||
| 파일 | 내용 |
|
||||
|------|------|
|
||||
| /var/log/nginx/api.sam.it.kr.access.log | API 접근 로그 |
|
||||
| /var/log/nginx/api.sam.it.kr.error.log | API 에러 로그 |
|
||||
| /var/log/nginx/sam.it.kr.access.log | 프론트엔드 접근 로그 |
|
||||
| /var/log/nginx/sam.it.kr.error.log | 프론트엔드 에러 로그 |
|
||||
| /var/log/nginx/mng.codebridge-x.com.access.log | Admin 접근 로그 |
|
||||
| /var/log/nginx/mng.codebridge-x.com.error.log | Admin 에러 로그 |
|
||||
| /var/log/nginx/sales.codebridge-x.com.access.log | Sales 접근 로그 |
|
||||
| /var/log/nginx/sales.codebridge-x.com.error.log | Sales 에러 로그 |
|
||||
|
||||
**주요 설정 값:**
|
||||
|
||||
- worker_processes: auto
|
||||
- worker_connections: 1024
|
||||
- client_max_body_size: 50M
|
||||
- keepalive_timeout: 65
|
||||
- gzip: on (text/plain, application/json, application/javascript, text/css)
|
||||
|
||||
---
|
||||
|
||||
## PHP-FPM
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status php8.4-fpm
|
||||
sudo systemctl reload php8.4-fpm # 무중단, 설정 변경 시
|
||||
sudo systemctl restart php8.4-fpm
|
||||
sudo systemctl stop php8.4-fpm
|
||||
sudo systemctl start php8.4-fpm
|
||||
```
|
||||
|
||||
**Pool 설정:**
|
||||
|
||||
| Pool | 설정 파일 | 소켓 | max_children | memory_limit |
|
||||
|------|----------|------|-------------|-------------|
|
||||
| api | /etc/php/8.4/fpm/pool.d/api.conf | /run/php/php8.4-fpm-api.sock | 10 | 128M |
|
||||
| admin | /etc/php/8.4/fpm/pool.d/admin.conf | /run/php/php8.4-fpm-admin.sock | 5 | 128M |
|
||||
| sales | /etc/php/8.4/fpm/pool.d/sales.conf | /run/php/php8.4-fpm-sales.sock | 3 | 128M |
|
||||
| api-stage | /etc/php/8.4/fpm/pool.d/api-stage.conf | /run/php/php8.4-fpm-api-stage.sock | 3 | 128M |
|
||||
|
||||
모든 Pool 공통 설정: upload_max_filesize=50M, post_max_size=50M, display_errors=Off
|
||||
|
||||
**로그:** /var/log/php8.4-fpm.log
|
||||
|
||||
---
|
||||
|
||||
## MySQL
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status mysql
|
||||
sudo systemctl restart mysql # 주의: 연결 끊김
|
||||
sudo systemctl stop mysql
|
||||
sudo systemctl start mysql
|
||||
|
||||
# 접속
|
||||
sudo mysql # root (auth_socket)
|
||||
mysql -u hskwon # hskwon (auth_socket, sudo 불필요)
|
||||
mysql -u codebridge -p sam # 앱 사용자
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /etc/mysql/mysql.conf.d/sam-tuning.cnf | 성능 튜닝 |
|
||||
| /etc/mysql/mysql.conf.d/mysqld.cnf | 기본 설정 |
|
||||
|
||||
**주요 튜닝 값:**
|
||||
|
||||
- innodb_buffer_pool_size: 2048M
|
||||
- innodb_log_file_size: 512M
|
||||
- innodb_flush_log_at_trx_commit: 2
|
||||
- max_connections: 100
|
||||
- slow_query_log: ON (long_query_time: 2s)
|
||||
|
||||
**로그:**
|
||||
|
||||
| 파일 | 내용 |
|
||||
|------|------|
|
||||
| /var/log/mysql/slow.log | 느린 쿼리 (2초 이상) |
|
||||
| /var/log/mysql/error.log | 에러 로그 |
|
||||
|
||||
**데이터베이스:**
|
||||
|
||||
| DB 이름 | 용도 |
|
||||
|---------|------|
|
||||
| sam | 메인 운영 DB |
|
||||
| sam_stage | Stage 환경 DB |
|
||||
| sam_stat | 통계 DB |
|
||||
| codebridge | Sales 레거시 DB |
|
||||
|
||||
---
|
||||
|
||||
## Redis
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status redis-server
|
||||
sudo systemctl restart redis-server
|
||||
sudo systemctl stop redis-server
|
||||
sudo systemctl start redis-server
|
||||
|
||||
redis-cli # CLI 접속
|
||||
redis-cli ping # 연결 테스트 → PONG
|
||||
```
|
||||
|
||||
**설정 파일:** /etc/redis/redis.conf
|
||||
|
||||
**주요 설정:**
|
||||
|
||||
- bind: 127.0.0.1 ::1 (로컬 전용)
|
||||
- maxmemory: 512mb
|
||||
- maxmemory-policy: allkeys-lru
|
||||
- supervised: systemd
|
||||
|
||||
**Redis CLI 유용한 명령어:**
|
||||
|
||||
```bash
|
||||
redis-cli info memory # 메모리 사용량
|
||||
redis-cli dbsize # 키 개수
|
||||
redis-cli keys '*' | head -20 # 키 확인 (운영 주의)
|
||||
redis-cli ttl "키이름" # TTL 확인
|
||||
redis-cli flushall # 전체 삭제 (주의: 세션도 삭제됨)
|
||||
```
|
||||
|
||||
**용도:**
|
||||
|
||||
| 기능 | 드라이버 | .env 설정 |
|
||||
|------|---------|----------|
|
||||
| 캐시 | Redis | CACHE_STORE=redis |
|
||||
| 세션 | Database | SESSION_DRIVER=database |
|
||||
| 큐 | Redis | Supervisor에서 `queue:work redis` 명시 |
|
||||
|
||||
---
|
||||
|
||||
## PM2 (Next.js)
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
pm2 status # 전체 상태
|
||||
pm2 reload sam-front # 운영 무중단 재시작 (cluster 모드)
|
||||
pm2 restart sam-front-stage # Stage 재시작
|
||||
pm2 logs sam-front --lines 100 # 로그 확인
|
||||
pm2 logs sam-front-stage --lines 100
|
||||
pm2 monit # 실시간 CPU/메모리
|
||||
pm2 describe sam-front # 상세 정보
|
||||
pm2 stop all # 전체 정지
|
||||
pm2 start all # 전체 시작
|
||||
cd /home/webservice && pm2 start ecosystem.config.js # 설정 파일로 시작
|
||||
pm2 save # 현재 상태 저장 (부팅 시 자동 복구용)
|
||||
```
|
||||
|
||||
**설정 파일:** /home/webservice/ecosystem.config.js
|
||||
|
||||
**프로세스 목록:**
|
||||
|
||||
| 프로세스명 | 모드 | 인스턴스 | 포트 | 메모리 제한 | 용도 |
|
||||
|-----------|------|---------|------|-----------|------|
|
||||
| sam-front | cluster | 2 | 3000 | 300M (max-old-space-size=256) | 운영 프론트엔드 |
|
||||
| sam-front-stage | fork | 1 | 3100 | 200M (max-old-space-size=128) | Stage 프론트엔드 |
|
||||
|
||||
**로그 파일:** ~/.pm2/logs/ (sam-front-out.log, sam-front-error.log 등)
|
||||
|
||||
---
|
||||
|
||||
## Supervisor (Queue Worker)
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
sudo supervisorctl status # 전체 상태
|
||||
sudo supervisorctl restart sam-queue-worker:* # 재시작
|
||||
sudo supervisorctl stop sam-queue-worker:* # 정지
|
||||
sudo supervisorctl start sam-queue-worker:* # 시작
|
||||
sudo supervisorctl reread # 설정 리로드
|
||||
sudo supervisorctl update
|
||||
```
|
||||
|
||||
**설정 파일:** /etc/supervisor/conf.d/sam-queue.conf
|
||||
|
||||
**프로세스 구성:**
|
||||
|
||||
- 프로그램명: sam-queue-worker
|
||||
- 프로세스 수: 2 (numprocs=2)
|
||||
- 실행 명령: `php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600`
|
||||
- 실행 사용자: www-data
|
||||
- 자동 재시작: true
|
||||
|
||||
**로그:** /home/webservice/api/shared/storage/logs/queue-worker.log
|
||||
|
||||
---
|
||||
|
||||
## node_exporter
|
||||
|
||||
```bash
|
||||
sudo systemctl status node_exporter
|
||||
sudo systemctl restart node_exporter
|
||||
curl -s localhost:9100/metrics | head -20 # 메트릭 확인
|
||||
```
|
||||
|
||||
**포트:** 9100 (UFW에서 CI/CD 서버 IP만 허용)
|
||||
|
||||
**역할:** CPU, RAM, 디스크, 네트워크 메트릭을 CI/CD 서버의 Prometheus에 제공.
|
||||
|
||||
---
|
||||
|
||||
## Certbot (SSL)
|
||||
|
||||
```bash
|
||||
sudo certbot certificates # 인증서 목록 및 만료일
|
||||
sudo systemctl status certbot.timer # 자동 갱신 타이머
|
||||
sudo certbot renew --dry-run # 갱신 시뮬레이션
|
||||
sudo certbot renew # 수동 갱신
|
||||
sudo certbot --nginx -d 도메인명 --email develop@codebridge-x.com # 새 도메인 발급
|
||||
```
|
||||
|
||||
자동 갱신은 systemd 타이머(certbot.timer)가 처리한다. 별도 crontab 불필요.
|
||||
|
||||
---
|
||||
|
||||
## fail2ban
|
||||
|
||||
```bash
|
||||
sudo systemctl status fail2ban
|
||||
sudo fail2ban-client status # jail 목록
|
||||
sudo fail2ban-client status sshd # SSH jail 상태 (차단 IP 목록)
|
||||
sudo fail2ban-client set sshd unbanip 차단된_IP주소 # IP 차단 해제
|
||||
sudo systemctl restart fail2ban
|
||||
```
|
||||
|
||||
**설정 파일:** /etc/fail2ban/jail.local (또는 jail.d/)
|
||||
|
||||
---
|
||||
|
||||
## UFW (방화벽)
|
||||
|
||||
```bash
|
||||
sudo ufw status verbose # 상태 확인 (규칙 목록)
|
||||
sudo ufw status numbered # 번호로 규칙 목록
|
||||
sudo ufw allow from IP주소 to any port 포트번호 # 규칙 추가
|
||||
sudo ufw delete 번호 # 규칙 삭제 (번호 기반)
|
||||
sudo ufw disable # 비활성화 (비상시만)
|
||||
sudo ufw enable # 활성화
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LibreOffice (문서 변환)
|
||||
|
||||
API 서버에서 문서 변환(Excel→PDF 등)에 사용. 헤드리스 모드로 동작.
|
||||
|
||||
**버전:** 24.2.7.2 (개발/운영 동일)
|
||||
|
||||
**명령어:**
|
||||
|
||||
```bash
|
||||
libreoffice --version # 버전 확인
|
||||
libreoffice --headless --convert-to pdf input.xlsx # CLI 변환 테스트
|
||||
```
|
||||
|
||||
**설치 패키지:**
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libreoffice-core libreoffice-writer libreoffice-calc libreoffice-impress
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 폰트
|
||||
|
||||
LibreOffice 문서 변환 시 폰트가 없으면 글자가 깨지므로 개발/운영 서버 동일하게 설치 필수.
|
||||
|
||||
**설치된 한글 폰트:**
|
||||
|
||||
| 폰트 | 설치 방식 | 경로 |
|
||||
|------|----------|------|
|
||||
| **Pretendard** (9 웨이트) | 수동 설치 (OTF) | `/usr/local/share/fonts/Pretendard-*.otf` |
|
||||
| **Nanum** (고딕/명조/스퀘어/손글씨 등) | apt (`fonts-nanum`, `fonts-nanum-extra`) | `/usr/share/fonts/truetype/nanum/` |
|
||||
| **Noto CJK** (Sans/Serif) | apt (`fonts-noto-cjk`) | `/usr/share/fonts/opentype/noto/` |
|
||||
|
||||
**폰트 관리 명령어:**
|
||||
|
||||
```bash
|
||||
fc-list :lang=ko family | sort -u # 설치된 한글 폰트 목록
|
||||
fc-list | grep -i pretendard # Pretendard 설치 확인
|
||||
sudo fc-cache -fv # 폰트 캐시 갱신 (새 폰트 추가 후 필수)
|
||||
```
|
||||
|
||||
**새 폰트 추가 시:**
|
||||
|
||||
```bash
|
||||
# 1. OTF/TTF 파일을 /usr/local/share/fonts/ 에 복사
|
||||
sudo cp *.otf /usr/local/share/fonts/
|
||||
|
||||
# 2. 폰트 캐시 갱신
|
||||
sudo fc-cache -fv
|
||||
|
||||
# 3. 확인
|
||||
fc-list | grep -i "폰트이름"
|
||||
```
|
||||
|
||||
> **주의:** 개발서버에 폰트를 추가하면 운영서버에도 동일하게 설치해야 변환 결과가 일치한다.
|
||||
|
||||
---
|
||||
|
||||
## SMTP (메일 발송)
|
||||
|
||||
Gmail SMTP를 통해 메일 발송. Google 앱 비밀번호 사용 (2단계 인증 필요).
|
||||
|
||||
**프로젝트별 SMTP 설정:**
|
||||
|
||||
| 항목 | api | mng |
|
||||
|------|-----|-----|
|
||||
| MAIL_HOST | smtp.gmail.com | smtp.gmail.com |
|
||||
| MAIL_PORT | 587 | 587 |
|
||||
| MAIL_USERNAME | shine1324@gmail.com | admin@codebridge-x.com |
|
||||
| MAIL_FROM_ADDRESS | shine1324@gmail.com | develop@codebridge-x.com |
|
||||
| MAIL_FROM_NAME | ${APP_NAME} | (주)코드브릿지엑스 |
|
||||
| MAIL_ENCRYPTION | tls | tls |
|
||||
|
||||
> **주의:** 개발/운영 서버의 MAIL_PASSWORD(앱 비밀번호)는 반드시 동일하게 유지.
|
||||
> Google 앱 비밀번호를 재발급하면 모든 서버에 동일하게 반영해야 한다.
|
||||
|
||||
**설정 파일 위치:**
|
||||
|
||||
| 프로젝트 | 운영 | 개발 |
|
||||
|---------|------|------|
|
||||
| api | `/home/webservice/api/shared/.env` | `/home/webservice/api/.env` |
|
||||
| mng | `/home/webservice/mng/shared/.env` | `/home/webservice/mng/.env` |
|
||||
|
||||
**변경 후 반영:**
|
||||
|
||||
```bash
|
||||
# api
|
||||
cd /home/webservice/api/current && php artisan config:cache
|
||||
|
||||
# mng
|
||||
cd /home/webservice/mng/current && php artisan config:cache
|
||||
```
|
||||
|
||||
**트러블슈팅:**
|
||||
|
||||
- `535 Username and Password not accepted` → 앱 비밀번호 만료 또는 불일치. 개발서버 값과 비교 후 동기화
|
||||
- `Connection refused` → 방화벽에서 587 포트 아웃바운드 차단 여부 확인
|
||||
- Google 앱 비밀번호 발급: Google 계정 → 보안 → 2단계 인증 → 앱 비밀번호
|
||||
363
dev/deploys/ops-manual/04-service-cicd.md
Normal file
363
dev/deploys/ops-manual/04-service-cicd.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# 4. CI/CD 서비스 관리
|
||||
|
||||
[목차로 돌아가기](./README.md) | 서버: sam-cicd (110.10.147.46)
|
||||
|
||||
---
|
||||
|
||||
## Jenkins
|
||||
|
||||
**서비스 제어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start jenkins
|
||||
sudo systemctl stop jenkins
|
||||
sudo systemctl restart jenkins
|
||||
sudo systemctl status jenkins
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /var/lib/jenkins/ | Jenkins 홈 (jobs, plugins, credentials) |
|
||||
| /etc/systemd/system/jenkins.service.d/override.conf | JVM 메모리 설정 |
|
||||
| /var/lib/jenkins/env-files/ | 배포 환경변수 (.env 파일) |
|
||||
| /var/lib/jenkins-agent/ | Agent 워크스페이스 (빌드 실행 격리) |
|
||||
| /etc/systemd/system/jenkins-agent.service | Agent systemd 서비스 |
|
||||
|
||||
**JVM 메모리 설정:**
|
||||
|
||||
```bash
|
||||
# /etc/systemd/system/jenkins.service.d/override.conf
|
||||
# Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true"
|
||||
|
||||
# 변경 후 적용
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart jenkins
|
||||
```
|
||||
|
||||
**로그:**
|
||||
|
||||
```bash
|
||||
sudo journalctl -u jenkins -f
|
||||
sudo journalctl -u jenkins --since "2 hours ago" --no-pager
|
||||
```
|
||||
|
||||
**웹 UI:** https://ci.sam.it.kr (관리자: hskwon)
|
||||
|
||||
### Credential 관리
|
||||
|
||||
| Credential ID | 유형 | 용도 |
|
||||
|--------------|------|------|
|
||||
| deploy-ssh-key | SSH Username with private key | 운영/개발서버 SSH 배포 |
|
||||
| gitea-api-token | Username with password | Gitea API 연동 (token을 username, 비밀번호 빈값) |
|
||||
|
||||
**Credential 위치:** Jenkins 관리 > Credentials > System > Global credentials
|
||||
|
||||
**SSH 키 경로:** /var/lib/jenkins/.ssh/id_ed25519
|
||||
|
||||
**환경변수 파일:**
|
||||
|
||||
```
|
||||
/var/lib/jenkins/env-files/
|
||||
react/
|
||||
.env.develop # 개발서버용
|
||||
.env.stage # Stage용
|
||||
.env.main # 운영용
|
||||
```
|
||||
|
||||
### 설치된 주요 플러그인
|
||||
|
||||
- Gitea Plugin -- Gitea Webhook 연동
|
||||
- SSH Agent Plugin -- SSH 키 기반 배포
|
||||
- Pipeline / Workflow Aggregator -- Jenkinsfile 지원
|
||||
- Pipeline Stage View -- 파이프라인 시각화
|
||||
- Blue Ocean -- 모던 UI
|
||||
- NodeJS Plugin -- Node.js 도구 관리 (22.22.0)
|
||||
|
||||
플러그인 업데이트 후 Jenkins 재시작이 필요한 경우: `sudo systemctl restart jenkins`
|
||||
|
||||
### Build Agent (분산 빌드)
|
||||
|
||||
Built-in Node의 executor는 0으로 설정되어 있으며, 빌드는 로컬 Agent(`local-agent`)에서 실행된다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Agent 이름 | local-agent |
|
||||
| Workspace | /var/lib/jenkins-agent/ |
|
||||
| Executor 수 | 2 |
|
||||
| 라벨 | build |
|
||||
| 연결 방식 | WebSocket (Inbound) |
|
||||
|
||||
**서비스 제어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start jenkins-agent
|
||||
sudo systemctl stop jenkins-agent
|
||||
sudo systemctl restart jenkins-agent
|
||||
sudo systemctl status jenkins-agent
|
||||
|
||||
# Agent 로그
|
||||
sudo journalctl -u jenkins-agent -f
|
||||
```
|
||||
|
||||
> **참고**: Jenkins 마스터 재시작 시 Agent가 자동 재연결된다. Agent가 연결 실패하면 `sudo systemctl restart jenkins-agent`로 수동 재시작.
|
||||
|
||||
### Workspace 정리
|
||||
|
||||
```bash
|
||||
# Agent workspace 용량 확인
|
||||
sudo du -sh /var/lib/jenkins-agent/workspace/*
|
||||
|
||||
# 특정 workspace 삭제
|
||||
sudo rm -rf /var/lib/jenkins-agent/workspace/<JOB_NAME>
|
||||
|
||||
# 전체 workspace 정리 (빌드 중이 아닌지 확인 후)
|
||||
sudo rm -rf /var/lib/jenkins-agent/workspace/*
|
||||
|
||||
# 레거시 Built-in workspace (이전 빌드 잔존 시)
|
||||
sudo du -sh /var/lib/jenkins/workspace/*
|
||||
sudo rm -rf /var/lib/jenkins/workspace/*
|
||||
|
||||
# 임시 파일 정리
|
||||
sudo find /tmp -name "jenkins*" -mtime +7 -delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gitea
|
||||
|
||||
**서비스 제어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start gitea
|
||||
sudo systemctl stop gitea
|
||||
sudo systemctl restart gitea
|
||||
sudo systemctl status gitea
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /etc/gitea/app.ini | 메인 설정 |
|
||||
| /var/lib/gitea/data/repositories/ | Git 저장소 데이터 |
|
||||
| /var/lib/gitea/log/ | Gitea 로그 |
|
||||
| /var/lib/gitea/custom/ | 커스텀 설정 |
|
||||
|
||||
**주요 설정 (app.ini):**
|
||||
|
||||
```ini
|
||||
[server]
|
||||
DOMAIN = git.sam.it.kr
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = https://git.sam.it.kr/
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true # 회원가입 비활성화
|
||||
REQUIRE_SIGNIN_VIEW = true # 로그인 필수
|
||||
```
|
||||
|
||||
**로그:**
|
||||
|
||||
```bash
|
||||
sudo journalctl -u gitea -f
|
||||
sudo tail -f /var/lib/gitea/log/gitea.log
|
||||
```
|
||||
|
||||
**웹 UI:** https://git.sam.it.kr (관리자: hskwon)
|
||||
|
||||
### 저장소 현황
|
||||
|
||||
| Organization | 저장소 | 설명 |
|
||||
|-------------|--------|------|
|
||||
| SamProject | sam-api | Laravel REST API |
|
||||
| SamProject | sam-manage | Laravel Admin (mng) |
|
||||
| SamProject | sam-react-prod | Next.js 프론트엔드 |
|
||||
| SamProject | sam-sales | 영업자 사이트 (레거시) |
|
||||
|
||||
### 사용자/조직 관리
|
||||
|
||||
- 사이트 관리: https://git.sam.it.kr/-/admin
|
||||
- 사용자 관리: https://git.sam.it.kr/-/admin/users
|
||||
- 조직 관리: https://git.sam.it.kr/-/admin/orgs
|
||||
|
||||
**CLI로 사용자 추가:**
|
||||
|
||||
```bash
|
||||
sudo -u git /usr/local/bin/gitea admin user create \
|
||||
--config /etc/gitea/app.ini \
|
||||
--username 사용자명 \
|
||||
--password 비밀번호 \
|
||||
--email 이메일 \
|
||||
--admin # 관리자 권한 (선택)
|
||||
```
|
||||
|
||||
### Webhook 설정
|
||||
|
||||
각 저장소에 Jenkins Webhook이 설정되어 있다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| URL | https://ci.sam.it.kr/gitea-webhook/post |
|
||||
| Content Type | application/json |
|
||||
| Events | Push Events |
|
||||
|
||||
**Webhook 확인/테스트:** 저장소 > Settings > Webhooks
|
||||
|
||||
### 개발서버 동기화 (post-receive hook)
|
||||
|
||||
개발서버 Gitea에서 CI/CD Gitea로 자동 동기화:
|
||||
|
||||
**Hook 위치 (개발서버):** `/data/GIT/samproject/<repo>.git/hooks/post-receive.d/push-to-cicd`
|
||||
|
||||
**토큰 파일 (개발서버):** `/data/GIT/.cicd-env` (chmod 600, owner: git)
|
||||
|
||||
| 저장소 | 동기화 브랜치 | 비고 |
|
||||
|--------|-------------|------|
|
||||
| sam-react-prod | main, develop | post-update hook 비활성화 (CI/CD가 개발서버 배포 담당) |
|
||||
| sam-api | main | develop은 기존 post-update hook 유지 |
|
||||
| sam-sales | main | |
|
||||
| sam-manage | main | 2026-02-24 hook 추가 |
|
||||
|
||||
> **참고:** react의 개발서버 배포는 Jenkins CI/CD 파이프라인이 처리한다.
|
||||
> 기존 post-update hook의 git pull 방식(`pull_react.sh`)은 비활성화됨 (2026-02-24).
|
||||
> 스크립트 위치: `/home/webservice/script/pull_react.sh`
|
||||
|
||||
**동기화 로그 확인:**
|
||||
|
||||
```bash
|
||||
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_react-prod.log"
|
||||
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_api.log"
|
||||
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_sales.log"
|
||||
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_manage.log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prometheus
|
||||
|
||||
**서비스 제어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start prometheus
|
||||
sudo systemctl stop prometheus
|
||||
sudo systemctl restart prometheus
|
||||
sudo systemctl status prometheus
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /etc/prometheus/prometheus.yml | 스크래핑 설정 |
|
||||
| /var/lib/prometheus/ | 시계열 데이터 |
|
||||
|
||||
**바인딩:** 127.0.0.1:9090 (외부 접근 차단)
|
||||
|
||||
**데이터 보존:** 30일 (--storage.tsdb.retention.time=30d)
|
||||
|
||||
**설정 변경 후 적용:**
|
||||
|
||||
```bash
|
||||
promtool check config /etc/prometheus/prometheus.yml # 문법 검사
|
||||
sudo systemctl restart prometheus
|
||||
# 또는 설정 리로드 (재시작 없이)
|
||||
curl -X POST http://localhost:9090/-/reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grafana
|
||||
|
||||
**서비스 제어:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start grafana-server
|
||||
sudo systemctl stop grafana-server
|
||||
sudo systemctl restart grafana-server
|
||||
sudo systemctl status grafana-server
|
||||
```
|
||||
|
||||
**설정 파일:**
|
||||
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| /etc/grafana/grafana.ini | 메인 설정 |
|
||||
| /var/lib/grafana/ | 대시보드 데이터, 플러그인 |
|
||||
|
||||
**주요 설정:**
|
||||
|
||||
```ini
|
||||
[server]
|
||||
http_port = 3100
|
||||
domain = monitor.sam.it.kr
|
||||
|
||||
[users]
|
||||
allow_sign_up = false
|
||||
```
|
||||
|
||||
**웹 UI:** https://monitor.sam.it.kr
|
||||
|
||||
---
|
||||
|
||||
## MySQL (CI/CD)
|
||||
|
||||
```bash
|
||||
sudo systemctl status mysql
|
||||
sudo systemctl restart mysql
|
||||
|
||||
# 접속
|
||||
mysql # hskwon (auth_socket)
|
||||
sudo mysql # root (auth_socket)
|
||||
```
|
||||
|
||||
**주요 튜닝 설정:**
|
||||
|
||||
```ini
|
||||
innodb_buffer_pool_size = 1536M
|
||||
max_connections = 50
|
||||
slow_query_log = 1
|
||||
long_query_time = 2
|
||||
```
|
||||
|
||||
**데이터베이스:** gitea (Gitea 데이터)
|
||||
|
||||
---
|
||||
|
||||
## Nginx (CI/CD)
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx # 무중단 리로드
|
||||
sudo systemctl restart nginx
|
||||
sudo systemctl status nginx
|
||||
```
|
||||
|
||||
**사이트 설정:**
|
||||
|
||||
| 파일 | 서비스 |
|
||||
|------|--------|
|
||||
| /etc/nginx/sites-available/git.sam.it.kr | Gitea 리버스 프록시 |
|
||||
| /etc/nginx/sites-available/ci.sam.it.kr | Jenkins 리버스 프록시 |
|
||||
| /etc/nginx/sites-available/monitor.sam.it.kr | Grafana 리버스 프록시 |
|
||||
|
||||
**git.sam.it.kr 주요 설정:**
|
||||
|
||||
```nginx
|
||||
client_max_body_size 500M; # 대용량 Git push 허용
|
||||
proxy_request_buffering off; # 스트리밍 전송 (413 방지)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## node_exporter / Certbot / fail2ban / UFW
|
||||
|
||||
운영서버와 동일한 명령어 체계. [운영서버 서비스 관리](./03-service-prod.md) 참조.
|
||||
|
||||
**UFW 규칙 (CI/CD):**
|
||||
|
||||
| 포트 | 프로토콜 | 용도 |
|
||||
|------|---------|------|
|
||||
| 22/tcp | ALLOW | SSH |
|
||||
| 80/tcp | ALLOW | HTTP |
|
||||
| 443/tcp | ALLOW | HTTPS |
|
||||
971
dev/deploys/ops-manual/05-deployment.md
Normal file
971
dev/deploys/ops-manual/05-deployment.md
Normal file
@@ -0,0 +1,971 @@
|
||||
# 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 | 운영 (직접) |
|
||||
|
||||
### Slack 알림 채널
|
||||
|
||||
| 채널 | 용도 | 알림 내용 |
|
||||
|------|------|----------|
|
||||
| `#product_infra` | 빌드/배포 상태 | 빌드 시작, 배포 성공/실패 |
|
||||
| `#product_deploy` | 운영 배포 승인 | Stage 배포 완료 후 승인 대기 알림 (Jenkins 승인 링크 포함) |
|
||||
|
||||
### 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. `#product_deploy` Slack 채널에 승인 대기 알림 전송
|
||||
5. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃)
|
||||
|
||||
> **동시 빌드 방지:** 모든 파이프라인에 `disableConcurrentBuilds()` 적용.
|
||||
> 같은 프로젝트에서 빌드가 동시에 2개 이상 돌지 않음.
|
||||
> 승인 대기 중 새 push 시 → 기존 빌드 Abort 후 새 빌드 자동 시작.
|
||||
|
||||
**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/<repo>.git/hooks/post-receive.d/push-to-cicd`
|
||||
토큰 환경변수: `/data/GIT/.cicd-env` (chmod 600, owner: git)
|
||||
|
||||
### Webhook 설정 (CI/CD Gitea → Jenkins)
|
||||
|
||||
각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI):
|
||||
|
||||
```
|
||||
Repository Settings → Webhooks → Add Webhook (Gitea)
|
||||
- URL: https://ci.sam.it.kr/gitea-webhook/post
|
||||
- Content Type: application/json
|
||||
- Secret: <webhook_secret>
|
||||
- Events: Push events
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 배포 흐름도
|
||||
|
||||
```
|
||||
개발자 로컬
|
||||
│ git push origin (develop / main)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자 origin │
|
||||
│ │
|
||||
│ post-receive hooks: │
|
||||
│ │
|
||||
│ ┌─ develop push ────────────────────────────────────────┐ │
|
||||
│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │
|
||||
│ │ → 빌드 결과 rsync → 개발서버 배포 │ │
|
||||
│ │ api → 기존 post-update hook (pull + migrate) │ │
|
||||
│ │ mng → 기존 post-update hook (pull + build) │ │
|
||||
│ │ sales → 기존 post-update hook (pull) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ main push (모든 저장소 자동) ────────────────────────┐ │
|
||||
│ │ react → hook: CI/CD Gitea push ──→ Jenkins │ │
|
||||
│ │ → Stage 빌드+배포 → 승인 → Production 재빌드 │ │
|
||||
│ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │
|
||||
│ │ → Stage rsync+배포 → 승인 → Production 배포 │ │
|
||||
│ │ mng → hook: CI/CD Gitea push ──→ Jenkins │ │
|
||||
│ │ → Production rsync + build │ │
|
||||
│ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │
|
||||
│ │ → Production rsync │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ Jenkins 승인 흐름 (react/api main) ─────────────────────────┐
|
||||
│ │
|
||||
│ Jenkins 빌드 시작 │
|
||||
│ │ │
|
||||
│ ├─ Stage 자동 배포 (react: .env.stage 빌드) │
|
||||
│ │ │
|
||||
│ ├─ 📢 #product_deploy Slack 알림 (승인 링크 포함) │
|
||||
│ │ │
|
||||
│ ├─ ⏸️ 승인 대기 (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 | APP_ENV | DEV_TOOLBAR |
|
||||
|------|---------|-------------|---------|-------------|
|
||||
| .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.main | https://api.sam.it.kr | https://sam.it.kr | production | false |
|
||||
|
||||
> `NEXT_PUBLIC_APP_ENV` 값으로 타이틀 접두사 결정: `development` → `[D]`, `local` → `[L]`, 그 외 → 없음
|
||||
|
||||
**rsync 주의:** trailing slash 사용 금지: `.next` (O), `.next/` (X)
|
||||
|
||||
**릴리즈 보관:** 운영 5개, Stage 3개
|
||||
|
||||
### Jenkinsfile (react/Jenkinsfile)
|
||||
|
||||
```groovy
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
environment {
|
||||
DEPLOY_USER = 'hskwon'
|
||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
script {
|
||||
env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
|
||||
}
|
||||
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Prepare Env') {
|
||||
steps {
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
// main: Stage 빌드 먼저 (승인 후 Production 재빌드)
|
||||
sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production"
|
||||
} else {
|
||||
def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}"
|
||||
sh "cp ${envFile} .env.production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.production
|
||||
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 {
|
||||
slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
|
||||
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.production"
|
||||
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.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current &&
|
||||
cd /home/webservice && pm2 reload sam-front &&
|
||||
cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **참고:** Next.js는 `NEXT_PUBLIC_*` 환경변수가 빌드 시 바인딩되므로,
|
||||
> Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다.
|
||||
> main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드.
|
||||
|
||||
> **환경파일:** Jenkins는 CI/CD 서버의 env-files를 `.env.production`으로 복사하여 빌드한다.
|
||||
> Next.js 우선순위: `.env.local` > `.env.production` > `.env`
|
||||
> 따라서 서버에 `.env.local`이 있으면 `.env.production`을 덮어쓰므로 `.env.local`은 사용하지 않는다.
|
||||
|
||||
### 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
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
environment {
|
||||
DEPLOY_USER = 'hskwon'
|
||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
script {
|
||||
env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
|
||||
}
|
||||
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
message: "🚀 *api* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
|
||||
// ── main → 운영서버 Stage 배포 ──
|
||||
stage('Deploy Stage') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env' \
|
||||
--exclude='storage/app' --exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api-stage/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
ln -sfn /home/webservice/api-stage/shared/.env .env &&
|
||||
ln -sfn /home/webservice/api-stage/shared/storage/app storage/app &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
php artisan config:cache &&
|
||||
php artisan route:cache &&
|
||||
php artisan view:cache &&
|
||||
php artisan migrate --force &&
|
||||
ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current &&
|
||||
sudo systemctl reload php8.4-fpm &&
|
||||
cd /home/webservice/api-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 운영 배포 승인 ──
|
||||
stage('Production Approval') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
message: "🔔 *api* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
|
||||
timeout(time: 24, unit: 'HOURS') {
|
||||
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr',
|
||||
ok: '운영 배포 진행'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── main → 운영서버 Production 배포 ──
|
||||
stage('Deploy Production') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env' \
|
||||
--exclude='storage/app' --exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/api/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
ln -sfn /home/webservice/api/shared/.env .env &&
|
||||
ln -sfn /home/webservice/api/shared/storage/app storage/app &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
php artisan config:cache &&
|
||||
php artisan route:cache &&
|
||||
php artisan view:cache &&
|
||||
php artisan migrate --force &&
|
||||
ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current &&
|
||||
sudo systemctl reload php8.4-fpm &&
|
||||
sudo supervisorctl restart sam-queue-worker:* &&
|
||||
cd /home/webservice/api/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// develop → Jenkins 관여 안함 (기존 post-update hook 유지)
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) &&
|
||||
[ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
|
||||
sudo systemctl reload php8.4-fpm
|
||||
' || true
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **참고:** Laravel은 런타임 .env를 사용하므로 Stage/Production 별도 빌드가 필요 없다.
|
||||
> 각 환경의 shared/.env가 심링크로 연결된다.
|
||||
|
||||
### 수동 배포 절차 (API Production)
|
||||
|
||||
> **참고:** CI/CD Gitea는 `REQUIRE_SIGNIN_VIEW = true` 설정이므로,
|
||||
> 수동 git clone 시 `https://사용자:비밀번호@git.sam.it.kr/...` 형식 또는
|
||||
> CI/CD 서버에서 rsync로 전송하는 방식을 사용한다.
|
||||
|
||||
```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. 필수 디렉토리 생성 (.gitignore에 의해 누락)
|
||||
cd /home/webservice/api/releases/$RELEASE_ID
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs
|
||||
|
||||
# 4. 의존성 설치
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
# 5. 캐시 생성
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
|
||||
# 6. 마이그레이션 (필요시)
|
||||
php artisan migrate --force
|
||||
|
||||
# 7. 심링크 전환 (이 시점에 배포 적용)
|
||||
ln -sfn /home/webservice/api/releases/$RELEASE_ID /home/webservice/api/current
|
||||
|
||||
# 8. 서비스 리로드
|
||||
sudo systemctl reload php8.4-fpm
|
||||
sudo supervisorctl restart sam-queue-worker:*
|
||||
|
||||
# 9. 오래된 릴리즈 정리 (최근 5개만 유지)
|
||||
cd /home/webservice/api/releases
|
||||
ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 수동 배포 절차 (API Stage)
|
||||
|
||||
```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
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan migrate --force
|
||||
|
||||
ln -sfn /home/webservice/api-stage/releases/$RELEASE_ID /home/webservice/api-stage/current
|
||||
sudo systemctl reload php8.4-fpm
|
||||
|
||||
# 최근 3개만 유지
|
||||
cd /home/webservice/api-stage/releases
|
||||
ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MNG (Laravel Admin) 배포
|
||||
|
||||
API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요.
|
||||
|
||||
> **참고: storage/logs 심링크 (2026-02-26 변경)**
|
||||
> MNG는 storage/logs를 shared로 심링크하여 배포 간 로그를 영속 보존한다.
|
||||
> 이전에는 `mkdir`로 릴리즈 디렉토리에 생성하여 배포마다 로그가 유실되었음.
|
||||
> 변경: `ln -sfn /home/webservice/mng/shared/storage/logs storage/logs`
|
||||
|
||||
### Jenkinsfile (mng/Jenkinsfile)
|
||||
|
||||
```groovy
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
environment {
|
||||
DEPLOY_USER = 'hskwon'
|
||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
script {
|
||||
env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
|
||||
}
|
||||
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
message: "🚀 *mng* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
|
||||
// ── main → 운영서버 Production ──
|
||||
stage('Deploy Production') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/mng/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env' \
|
||||
--exclude='storage/app' --exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||
--exclude='node_modules' \
|
||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/mng/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/mng/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} &&
|
||||
ln -sfn /home/webservice/mng/shared/.env .env &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/app storage/app &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
npm install --prefer-offline &&
|
||||
npm run build &&
|
||||
php artisan config:cache &&
|
||||
php artisan route:cache &&
|
||||
php artisan view:cache &&
|
||||
php artisan migrate --force &&
|
||||
ln -sfn /home/webservice/mng/releases/${RELEASE_ID} /home/webservice/mng/current &&
|
||||
sudo systemctl reload php8.4-fpm &&
|
||||
cd /home/webservice/mng/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// develop → Jenkins 관여 안함 (기존 post-update hook 유지)
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *mng* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *mng* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
PREV=\$(ls -1dt /home/webservice/mng/releases/*/ | sed -n "2p" | xargs basename) &&
|
||||
[ -n "\$PREV" ] && ln -sfn /home/webservice/mng/releases/\$PREV /home/webservice/mng/current &&
|
||||
sudo systemctl reload php8.4-fpm
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 수동 배포
|
||||
|
||||
```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/.env /home/webservice/mng/releases/$RELEASE_ID/.env
|
||||
ln -sfn /home/webservice/mng/shared/storage/app /home/webservice/mng/releases/$RELEASE_ID/storage/app
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/releases/$RELEASE_ID/storage/logs
|
||||
|
||||
cd /home/webservice/mng/releases/$RELEASE_ID
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions}
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
# Vite 빌드 (Blade + Tailwind)
|
||||
npm install --prefer-offline
|
||||
npm run build
|
||||
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan migrate --force
|
||||
|
||||
ln -sfn /home/webservice/mng/releases/$RELEASE_ID /home/webservice/mng/current
|
||||
sudo systemctl reload php8.4-fpm
|
||||
|
||||
# 오래된 릴리즈 정리
|
||||
cd /home/webservice/mng/releases
|
||||
ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sales (Plain PHP) 배포
|
||||
|
||||
레거시 PHP 애플리케이션. rsync 기반 배포.
|
||||
|
||||
### Jenkinsfile (sales/Jenkinsfile)
|
||||
|
||||
```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.production
|
||||
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.production hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production
|
||||
|
||||
# 심링크 전환 및 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} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
php artisan config:cache &&
|
||||
php artisan route:cache &&
|
||||
php artisan view:cache &&
|
||||
php artisan migrate --force &&
|
||||
ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current &&
|
||||
sudo systemctl reload php8.4-fpm &&
|
||||
sudo supervisorctl restart sam-queue-worker:*
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 배포 후 확인 사항
|
||||
|
||||
```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
|
||||
sudo tail -20 /home/webservice/mng/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/<JOB_NAME>/<BUILD_NUMBER>/console
|
||||
```
|
||||
|
||||
**빌드 실패 주요 원인:**
|
||||
|
||||
1. npm install 실패 -- node_modules 캐시, 네트워크
|
||||
2. npm run build 실패 -- TypeScript 오류, 환경변수 누락
|
||||
3. rsync 실패 -- SSH 키 문제, 디스크 공간 부족
|
||||
4. composer install 실패 -- 네트워크, PHP 확장 누락
|
||||
5. SSH 연결 실패 -- known_hosts 변경, 키 만료
|
||||
6. Laravel `package:discover` 실패 -- `bootstrap/cache/` 디렉토리 누락 (`.gitignore`에 포함)
|
||||
7. Blade view 캐시 실패 -- `storage/framework/views/` 디렉토리 누락
|
||||
8. `Target class [request] does not exist` -- CLI 컨텍스트에서 `request()` 호출 (AppServiceProvider 확인)
|
||||
|
||||
> **Laravel 배포 필수:** `mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs`를
|
||||
> `composer install` 전에 실행해야 함. `.gitignore`가 이 디렉토리들을 제외하므로 rsync/git clone 후 생성 필요.
|
||||
203
dev/deploys/ops-manual/06-database.md
Normal file
203
dev/deploys/ops-manual/06-database.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 6. 데이터베이스 관리
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## [운영] MySQL 접속
|
||||
|
||||
```bash
|
||||
sudo mysql # root (auth_socket)
|
||||
mysql -u hskwon # 관리자 (auth_socket, sudo 불필요)
|
||||
mysql -u codebridge -p sam # 앱 사용자
|
||||
```
|
||||
|
||||
## [CI/CD] MySQL 접속
|
||||
|
||||
```bash
|
||||
mysql # hskwon (auth_socket)
|
||||
sudo mysql # root (auth_socket)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DB 백업
|
||||
|
||||
### [운영] 수동 백업
|
||||
|
||||
```bash
|
||||
# sam DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# sam_stat DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers sam_stat | gzip > /tmp/sam_stat_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# codebridge DB (Sales)
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# 전체 DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers --all-databases | gzip > /tmp/all_db_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# 특정 테이블만
|
||||
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에 접속.
|
||||
|
||||
**스크립트:** /home/hskwon/scripts/backup-db.sh
|
||||
**저장소:** /home/hskwon/backups/mysql/
|
||||
**보존:** 14일
|
||||
|
||||
```bash
|
||||
# 수동 원격 백업
|
||||
ssh sam-prod "mysqldump --single-transaction --routines sam" | gzip \
|
||||
> /home/hskwon/backups/mysql/sam_production_$(date +%Y%m%d).sql.gz
|
||||
```
|
||||
|
||||
### [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
|
||||
```
|
||||
|
||||
### 백업 파일 외부 전송
|
||||
|
||||
```bash
|
||||
# 운영서버 -> CI/CD 서버
|
||||
scp /tmp/sam_*.sql.gz sam-cicd:/home/hskwon/backups/mysql/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DB 복구
|
||||
|
||||
### [운영]
|
||||
|
||||
```bash
|
||||
# 전체 DB 복구
|
||||
gunzip -c /path/to/sam_백업파일.sql.gz | sudo mysql sam
|
||||
|
||||
# 특정 테이블 복구
|
||||
sudo mysql sam < /path/to/sam_테이블명_백업파일.sql
|
||||
```
|
||||
|
||||
### [CI/CD] Gitea DB 복구
|
||||
|
||||
```bash
|
||||
gunzip -c /home/hskwon/backups/mysql/gitea_YYYYMMDD_HHMMSS.sql.gz | mysql gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slow Query 분석 (운영)
|
||||
|
||||
```bash
|
||||
# 로그 직접 확인
|
||||
sudo tail -100 /var/log/mysql/slow.log
|
||||
|
||||
# 요약 분석 (상위 10개, 횟수 기준)
|
||||
sudo mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
|
||||
|
||||
# 요약 분석 (소요 시간 기준)
|
||||
sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자주 사용하는 MySQL 명령어
|
||||
|
||||
```sql
|
||||
-- 현재 프로세스 목록
|
||||
SHOW PROCESSLIST;
|
||||
|
||||
-- 현재 연결 수
|
||||
SHOW STATUS LIKE 'Threads_connected';
|
||||
|
||||
-- 최대 연결 수
|
||||
SHOW VARIABLES LIKE 'max_connections';
|
||||
|
||||
-- InnoDB 상태
|
||||
SHOW ENGINE INNODB STATUS\G
|
||||
|
||||
-- 테이블 크기 확인 (sam DB)
|
||||
SELECT table_name, ROUND(data_length/1024/1024, 2) AS data_mb,
|
||||
ROUND(index_length/1024/1024, 2) AS index_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'sam'
|
||||
ORDER BY data_length DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- 실행 중인 쿼리 확인
|
||||
SELECT id, user, host, db, command, time, state, info
|
||||
FROM information_schema.processlist
|
||||
WHERE command != 'Sleep'
|
||||
ORDER BY time DESC;
|
||||
|
||||
-- 느린 쿼리 kill
|
||||
KILL 프로세스_ID;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DB 사용자 관리
|
||||
|
||||
```sql
|
||||
-- 사용자 목록
|
||||
SELECT user, host, plugin FROM mysql.user;
|
||||
|
||||
-- 사용자 권한 확인
|
||||
SHOW GRANTS FOR 'codebridge'@'localhost';
|
||||
|
||||
-- 비밀번호 변경
|
||||
ALTER USER 'codebridge'@'localhost' IDENTIFIED BY '새_비밀번호';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redis 관리 (운영서버)
|
||||
|
||||
### 기본 명령
|
||||
|
||||
```bash
|
||||
redis-cli info memory # 메모리 사용량
|
||||
redis-cli dbsize # 키 개수
|
||||
redis-cli --bigkeys # 가장 큰 키 확인
|
||||
redis-cli info keyspace # 키 통계
|
||||
redis-cli info commandstats | head -20 # 명령어 실행 통계
|
||||
```
|
||||
|
||||
### 캐시 정리
|
||||
|
||||
```bash
|
||||
# Laravel 캐시 삭제 (artisan)
|
||||
cd /home/webservice/api/current
|
||||
php artisan cache:clear
|
||||
|
||||
# 특정 접두어 키 삭제
|
||||
redis-cli keys "laravel_cache:*" | xargs redis-cli del
|
||||
|
||||
# 전체 초기화 (세션도 삭제됨 - 주의)
|
||||
redis-cli flushall
|
||||
```
|
||||
|
||||
### 설정 임시 변경
|
||||
|
||||
```bash
|
||||
# maxmemory 임시 증가 (재시작 불필요)
|
||||
redis-cli config set maxmemory 768mb
|
||||
|
||||
# maxmemory 확인
|
||||
redis-cli config get maxmemory
|
||||
```
|
||||
|
||||
### 실시간 모니터링
|
||||
|
||||
```bash
|
||||
# 실시간 명령어 모니터링 (부하 주의)
|
||||
redis-cli monitor
|
||||
# Ctrl+C로 중단
|
||||
```
|
||||
271
dev/deploys/ops-manual/07-monitoring.md
Normal file
271
dev/deploys/ops-manual/07-monitoring.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# 7. 모니터링
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
운영서버 (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100
|
||||
개발서버 (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100
|
||||
CI/CD (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100
|
||||
```
|
||||
|
||||
- **Grafana 대시보드:** https://monitor.sam.it.kr
|
||||
- **Prometheus 쿼리:** CI/CD 서버에서 http://localhost:9090
|
||||
- **운영서버 메트릭:** 운영서버에서 http://localhost:9100/metrics
|
||||
- **개발서버 메트릭:** 개발서버에서 http://localhost:9100/metrics
|
||||
|
||||
---
|
||||
|
||||
## Prometheus 스크래핑 설정
|
||||
|
||||
**현재 설정 (/etc/prometheus/prometheus.yml):**
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'sam-prod'
|
||||
static_configs:
|
||||
- targets: ['211.117.60.189:9100']
|
||||
labels:
|
||||
server: 'production'
|
||||
|
||||
- job_name: 'sam-cicd'
|
||||
static_configs:
|
||||
- targets: ['localhost:9100']
|
||||
labels:
|
||||
server: 'cicd'
|
||||
|
||||
- job_name: 'sam-dev'
|
||||
static_configs:
|
||||
- targets: ['114.203.209.83:9100']
|
||||
labels:
|
||||
server: 'development'
|
||||
```
|
||||
|
||||
### 스크래핑 대상 추가
|
||||
|
||||
```bash
|
||||
# 1. 대상 서버에 node_exporter 설치 (미설치 시)
|
||||
# 바이너리: https://github.com/prometheus/node_exporter/releases
|
||||
# 서비스: /etc/systemd/system/node_exporter.service
|
||||
# 포트: 9100 (기본)
|
||||
|
||||
# 2. 대상 서버 방화벽에서 CI/CD IP 허용
|
||||
sudo ufw allow from 110.10.147.46 to any port 9100 comment 'Prometheus scraping from CI/CD'
|
||||
|
||||
# 3. CI/CD 서버에서 설정 파일 편집
|
||||
sudo vim /etc/prometheus/prometheus.yml
|
||||
|
||||
# 4. 새 대상 추가 예시
|
||||
# - job_name: 'sam-new'
|
||||
# static_configs:
|
||||
# - targets: ['<서버IP>:9100']
|
||||
# labels:
|
||||
# server: '<환경명>'
|
||||
|
||||
# 5. 문법 검사
|
||||
promtool check config /etc/prometheus/prometheus.yml
|
||||
|
||||
# 6. 서비스 리로드
|
||||
sudo systemctl restart prometheus
|
||||
```
|
||||
|
||||
### 대상 상태 확인
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:9090/api/v1/targets | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for t in data['data']['activeTargets']:
|
||||
print(f\"{t['labels'].get('job','?'):15} {t['health']:6} {t['scrapeUrl']}\")
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PromQL 쿼리
|
||||
|
||||
Prometheus UI (http://localhost:9090) 또는 Grafana에서 사용.
|
||||
|
||||
### CPU
|
||||
|
||||
```promql
|
||||
# CPU 사용률 (%) - 서버별
|
||||
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
|
||||
|
||||
# 유휴 CPU 비율 (5분 평균)
|
||||
rate(node_cpu_seconds_total{mode="idle"}[5m])
|
||||
```
|
||||
|
||||
### 메모리
|
||||
|
||||
```promql
|
||||
# 사용 가능 메모리 비율
|
||||
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
|
||||
|
||||
# 사용 중인 메모리 (GB)
|
||||
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / 1024 / 1024 / 1024
|
||||
|
||||
# 전체 메모리 (GB)
|
||||
node_memory_MemTotal_bytes / 1024 / 1024 / 1024
|
||||
```
|
||||
|
||||
### 디스크
|
||||
|
||||
```promql
|
||||
# 디스크 사용률 (%)
|
||||
100 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100)
|
||||
|
||||
# 사용 가능 디스크 (GB)
|
||||
node_filesystem_avail_bytes{mountpoint="/"} / 1024 / 1024 / 1024
|
||||
|
||||
# 디스크 I/O (읽기/쓰기 바이트, 5분 평균)
|
||||
rate(node_disk_read_bytes_total[5m])
|
||||
rate(node_disk_written_bytes_total[5m])
|
||||
```
|
||||
|
||||
### 네트워크
|
||||
|
||||
```promql
|
||||
# 수신 (bytes/sec, 5분 평균)
|
||||
rate(node_network_receive_bytes_total{device="eth0"}[5m])
|
||||
|
||||
# 전송 (bytes/sec, 5분 평균)
|
||||
rate(node_network_transmit_bytes_total{device="eth0"}[5m])
|
||||
```
|
||||
|
||||
### 시스템
|
||||
|
||||
```promql
|
||||
# 서버 업타임 (초)
|
||||
time() - node_boot_time_seconds
|
||||
|
||||
# Load Average (1분)
|
||||
node_load1
|
||||
|
||||
# 열린 파일 디스크립터
|
||||
node_filefd_allocated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grafana 대시보드
|
||||
|
||||
**기본 대시보드:** Node Exporter Full (ID: 1860)
|
||||
|
||||
**Data Source:** Prometheus (http://localhost:9090)
|
||||
|
||||
### 대시보드 추가 (Import)
|
||||
|
||||
1. Grafana 웹 > Dashboards > Import
|
||||
2. Dashboard ID 입력 (예: 1860)
|
||||
3. Data Source로 Prometheus 선택
|
||||
4. Import 클릭
|
||||
|
||||
### 알림 규칙 설정
|
||||
|
||||
**설정 경로:** Grafana > Alerting > Alert rules
|
||||
|
||||
**현재 설정된 알림 규칙 (SAM Alerts 폴더):**
|
||||
|
||||
| 규칙명 | 조건 | 대기 시간 | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| CPU 사용률 > 90% | avg(rate(node_cpu_idle[5m])) | 5분 | CPU 과부하 |
|
||||
| 메모리 사용률 > 85% | MemAvailable/MemTotal | 5분 | 메모리 부족 |
|
||||
| 디스크 사용률 > 80% | filesystem_avail/size (/) | 5분 | 디스크 공간 부족 |
|
||||
| 서비스 다운 (스크래핑 실패) | up < 1 | 1분 | Prometheus 타겟 다운 |
|
||||
|
||||
**알림 채널:** Grafana > Alerting > Contact points 에서 이메일, Slack 등 설정
|
||||
|
||||
**현재 설정:** SAM Slack Contact Point (Incoming Webhook) 연결 완료. Notification Policy에서 SAM Alerts 폴더의 알림이 Slack `#product_infra` 채널로 전송됨.
|
||||
|
||||
---
|
||||
|
||||
## [운영] 성능 모니터링
|
||||
|
||||
### 메모리 사용량 분석
|
||||
|
||||
```bash
|
||||
free -h
|
||||
ps aux --sort=-%mem | head -16
|
||||
|
||||
# MySQL 메모리
|
||||
sudo mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
|
||||
sudo mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_bytes_data';"
|
||||
|
||||
# Redis 메모리
|
||||
redis-cli info memory | grep -E "used_memory_human|maxmemory_human"
|
||||
|
||||
# PHP-FPM 프로세스별 메모리
|
||||
ps -C php-fpm8.4 -o pid,user,%mem,rss,args --sort=-rss
|
||||
```
|
||||
|
||||
### CPU 모니터링
|
||||
|
||||
```bash
|
||||
htop
|
||||
uptime # 로드 평균 (1분/5분/15분)
|
||||
ps aux --sort=-%cpu | head -11 # CPU 상위 프로세스
|
||||
nproc # CPU 코어 수
|
||||
```
|
||||
|
||||
### 디스크 I/O
|
||||
|
||||
```bash
|
||||
df -h
|
||||
sudo du -sh /home/webservice/*
|
||||
sudo du -sh /var/log/*
|
||||
sudo du -sh /var/lib/mysql/*
|
||||
sudo iostat -x 1 5 # 실시간 I/O
|
||||
```
|
||||
|
||||
### 네트워크
|
||||
|
||||
```bash
|
||||
sudo ss -tlnp # 열린 포트
|
||||
ss -s # 연결 상태 요약
|
||||
sudo ss -tn | awk '{print $4}' | grep -oP ':\d+$' | sort | uniq -c | sort -rn | head -10
|
||||
```
|
||||
|
||||
### PHP-FPM Pool 상태
|
||||
|
||||
```bash
|
||||
ps aux | grep "php-fpm" | grep -v grep | wc -l # 프로세스 수
|
||||
ps aux | grep "php-fpm" | grep -v grep | awk '{print $NF}' | sort | uniq -c # Pool별
|
||||
sudo grep "max_children" /var/log/php8.4-fpm.log | tail -10 # max_children 도달 여부
|
||||
```
|
||||
|
||||
### MySQL 성능
|
||||
|
||||
```bash
|
||||
# 연결 상태
|
||||
sudo mysql -e "SHOW STATUS LIKE 'Threads%';"
|
||||
|
||||
# Slow Query 요약
|
||||
sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
|
||||
|
||||
# InnoDB Buffer Pool 히트율
|
||||
sudo mysql -e "
|
||||
SELECT
|
||||
ROUND((1 - (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_reads') /
|
||||
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests')) * 100, 2) AS buffer_pool_hit_rate_pct;
|
||||
"
|
||||
|
||||
# 테이블 락 대기
|
||||
sudo mysql -e "SHOW STATUS LIKE 'Table_locks%';"
|
||||
```
|
||||
|
||||
### PM2 모니터링
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
pm2 monit # 실시간 CPU/메모리
|
||||
pm2 describe sam-front # 상세 정보
|
||||
pm2 describe sam-front | grep -A5 "restart" # 재시작 이력
|
||||
```
|
||||
787
dev/deploys/ops-manual/08-troubleshooting.md
Normal file
787
dev/deploys/ops-manual/08-troubleshooting.md
Normal file
@@ -0,0 +1,787 @@
|
||||
# 8. 장애 대응 가이드
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## 운영서버 장애
|
||||
|
||||
### Nginx 502 Bad Gateway
|
||||
|
||||
**증상:** 브라우저에서 502 에러. 정적 파일은 정상, 동적 요청만 실패.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo tail -50 /var/log/nginx/api.sam.it.kr.error.log
|
||||
# "connect() failed" 또는 "no live upstreams" 메시지 확인
|
||||
|
||||
# Laravel 사이트인 경우
|
||||
sudo systemctl status php8.4-fpm
|
||||
ls -la /run/php/php8.4-fpm-*.sock
|
||||
|
||||
# Next.js 사이트인 경우
|
||||
pm2 status
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
# PHP-FPM이 죽은 경우
|
||||
sudo systemctl restart php8.4-fpm
|
||||
|
||||
# PM2가 죽은 경우
|
||||
cd /home/webservice && pm2 start ecosystem.config.js
|
||||
pm2 save
|
||||
|
||||
# Nginx 자체 문제
|
||||
sudo nginx -t && sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
**예방:** PHP-FPM과 PM2는 자동 재시작 설정됨. 반복 발생 시 메모리 부족을 의심.
|
||||
|
||||
---
|
||||
|
||||
### Nginx 504 Gateway Timeout
|
||||
|
||||
**증상:** 요청이 오래 걸린 후 504 에러. 무거운 API 호출에서 발생.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo tail -50 /var/log/nginx/api.sam.it.kr.error.log
|
||||
# "upstream timed out" 메시지 확인
|
||||
sudo tail -50 /var/log/mysql/slow.log
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
# 장시간 실행 중인 MySQL 쿼리 kill
|
||||
sudo mysql -e "SHOW PROCESSLIST;" | grep -v Sleep
|
||||
sudo mysql -e "KILL 프로세스_ID;"
|
||||
|
||||
# Nginx timeout 일시적 증가 (필요시)
|
||||
# /etc/nginx/sites-available/api.sam.it.kr 에서 fastcgi_read_timeout 값 조정
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
**예방:** 무거운 작업은 Queue로 처리. 현재 fastcgi_read_timeout은 60초.
|
||||
|
||||
---
|
||||
|
||||
### MySQL 연결 거부 / Too Many Connections
|
||||
|
||||
**증상:** "Connection refused" 또는 "Too many connections" 에러.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status mysql
|
||||
sudo mysql -e "SHOW STATUS LIKE 'Threads_connected';"
|
||||
sudo mysql -e "SHOW VARIABLES LIKE 'max_connections';"
|
||||
sudo mysql -e "SHOW PROCESSLIST;"
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
# MySQL이 정지된 경우
|
||||
sudo systemctl start mysql
|
||||
|
||||
# Sleep 연결 정리 (300초 이상 유휴)
|
||||
sudo mysql -e "SELECT id FROM information_schema.processlist WHERE command='Sleep' AND time > 300;" | while read id; do
|
||||
[ "$id" != "id" ] && sudo mysql -e "KILL $id;"
|
||||
done
|
||||
|
||||
# 임시로 max_connections 증가 (재시작 없이)
|
||||
sudo mysql -e "SET GLOBAL max_connections = 150;"
|
||||
```
|
||||
|
||||
**예방:** max_connections(100)은 현재 규모에 적합. 부족 시 sam-tuning.cnf 조정.
|
||||
|
||||
---
|
||||
|
||||
### [개발] MySQL 8.4 인증 플러그인 오류
|
||||
|
||||
**증상:** `SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client`
|
||||
|
||||
**원인:** MySQL 8.4에서 `mysql_native_password` 플러그인이 기본 비활성화됨. 레거시 PHP(5130 등)의 mysqlnd가 `caching_sha2_password`를 지원하지 못함.
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
# 1. mysqld.cnf에 플러그인 활성화 추가
|
||||
sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
|
||||
# [mysqld] 섹션에 추가:
|
||||
# mysql_native_password=ON
|
||||
|
||||
# 2. MySQL 재시작
|
||||
sudo systemctl restart mysql
|
||||
|
||||
# 3. 레거시 PHP용 계정 인증 방식 변경
|
||||
mysql -u debian-sys-maint -p'비밀번호' -e "
|
||||
ALTER USER '계정'@'localhost' IDENTIFIED WITH mysql_native_password BY '비밀번호';
|
||||
FLUSH PRIVILEGES;"
|
||||
```
|
||||
|
||||
**실제 사례 (2026-02-25):**
|
||||
|
||||
1. 5130 레거시 사이트 로그인 시 2054 에러 발생
|
||||
2. `/etc/mysql/mysql.conf.d/mysqld.cnf`에 `mysql_native_password=ON` 추가 후 MySQL 재시작
|
||||
3. `codebridge`, `pro`, `chandj` 계정을 `mysql_native_password`로 변경하여 해결
|
||||
|
||||
**참고:** debian-sys-maint 비밀번호는 `/etc/mysql/debian.cnf`에서 확인 가능.
|
||||
|
||||
---
|
||||
|
||||
### .env 권한 오류로 전체 500 에러 (API + MNG)
|
||||
|
||||
**증상:** api.sam.it.kr, mng.sam.it.kr 모든 요청에서 500 에러. PHP-FPM, Redis, MySQL 모두 정상.
|
||||
|
||||
**원인:** `.env` 파일 권한이 `600`(`-rw-------`)으로 변경되어 PHP-FPM(`www-data`)이 읽지 못함. 모든 환경변수가 null이 되면서 `DB_CONNECTION`, `CACHE_STORE` 등이 기본값(SQLite)로 fallback.
|
||||
|
||||
**주요 발생 원인:**
|
||||
- `vi`로 `.env` 편집 시 파일이 재생성되면서 umask에 따라 `600`으로 변경됨
|
||||
- 배포 스크립트에서 권한 미설정
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
# 1. .env 권한 확인 (640이어야 정상)
|
||||
ls -la /home/webservice/api/shared/.env
|
||||
ls -la /home/webservice/mng/shared/.env
|
||||
|
||||
# 2. www-data로 읽기 테스트
|
||||
sudo -u www-data cat /home/webservice/api/shared/.env | head -1
|
||||
# "Permission denied" → 권한 문제 확인
|
||||
|
||||
# 3. artisan으로 config 확인 (CLI는 sudo로 정상 작동)
|
||||
sudo php /home/webservice/api/current/artisan config:show cache
|
||||
# default가 database/file이면 .env 미반영 상태
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo chmod 640 /home/webservice/api/shared/.env
|
||||
sudo chmod 640 /home/webservice/mng/shared/.env
|
||||
```
|
||||
|
||||
**예방:**
|
||||
- 서버 계정 `~/.vimrc`에 `set backupcopy=yes` 추가 (vi 편집 시 권한 유지)
|
||||
- 배포 스크립트에 `chmod 640` 포함
|
||||
- 자세한 내용: 09-security.md "[운영] .env 파일 보안" 참조
|
||||
|
||||
**실제 사례 (2026-03-03):**
|
||||
|
||||
1. `.env`에서 `GEMINI_MODEL` 값을 `vi`로 변경
|
||||
2. `vi`가 파일을 재생성하면서 권한 `600`으로 변경
|
||||
3. PHP-FPM(`www-data`)이 `.env` 읽기 실패 → 모든 env 값 null
|
||||
4. `CACHE_STORE=null` → 기본값 `database` → SQLite 연결 시도 → 500 에러
|
||||
5. `chmod 640`으로 권한 복원하여 즉시 해결
|
||||
|
||||
**진단 포인트:**
|
||||
- CLI(`artisan tinker`)에서는 정상인데 웹만 500 → **파일 권한 문제 의심**
|
||||
- Laravel 로그에 기록이 없으면 **로그 파일 쓰기 권한도 확인**
|
||||
|
||||
---
|
||||
|
||||
### Redis 메모리 부족
|
||||
|
||||
**증상:** "OOM command not allowed" 메시지.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
redis-cli info memory | grep used_memory_human
|
||||
redis-cli config get maxmemory
|
||||
redis-cli dbsize
|
||||
redis-cli --bigkeys
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
cd /home/webservice/api/current && php artisan cache:clear
|
||||
redis-cli keys "laravel_cache:*" | xargs redis-cli del
|
||||
redis-cli flushall # 전체 초기화 (세션도 삭제 - 주의)
|
||||
redis-cli config set maxmemory 768mb # 임시 증가
|
||||
```
|
||||
|
||||
**예방:** allkeys-lru 정책 설정됨. 512MB 부족 시 redis.conf에서 maxmemory 조정.
|
||||
|
||||
---
|
||||
|
||||
### PM2 프로세스 크래시 / 재시작 반복
|
||||
|
||||
**증상:** sam.it.kr 접속 불가 또는 간헐적 502. PM2 status에서 restart 횟수 급증.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
pm2 logs sam-front --err --lines 100
|
||||
pm2 describe sam-front | grep memory
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
pm2 reload sam-front
|
||||
|
||||
# 문제 지속 시 완전 재시작
|
||||
pm2 stop sam-front
|
||||
cd /home/webservice && pm2 start ecosystem.config.js --only sam-front
|
||||
pm2 save
|
||||
|
||||
# 로그 파일이 너무 큰 경우
|
||||
pm2 flush
|
||||
```
|
||||
|
||||
**예방:** max_memory_restart=300M 설정됨. 반복 크래시 시 코드 문제 조사.
|
||||
|
||||
---
|
||||
|
||||
### Queue Worker 정지 / 미처리
|
||||
|
||||
**증상:** 이메일, 알림 등 비동기 작업 미처리.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo supervisorctl status
|
||||
sudo tail -50 /home/webservice/api/shared/storage/logs/queue-worker.log
|
||||
cd /home/webservice/api/current && php artisan queue:monitor redis:default
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo supervisorctl restart sam-queue-worker:*
|
||||
|
||||
cd /home/webservice/api/current
|
||||
php artisan queue:failed # 실패한 작업 확인
|
||||
php artisan queue:retry all # 실패한 작업 재시도
|
||||
php artisan queue:flush # 실패한 작업 전체 삭제
|
||||
```
|
||||
|
||||
**예방:** max-time=3600 설정 (1시간마다 자동 재시작). Supervisor가 프로세스 자동 복구.
|
||||
|
||||
---
|
||||
|
||||
### SSL 인증서 만료
|
||||
|
||||
**증상:** 브라우저에서 "연결이 비공개가 아닙니다" 경고.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo certbot certificates
|
||||
sudo systemctl status certbot.timer
|
||||
echo | openssl s_client -servername api.sam.it.kr -connect 211.117.60.189:443 2>/dev/null | openssl x509 -noout -dates
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo certbot renew
|
||||
sudo certbot certonly --nginx -d api.sam.it.kr # 특정 도메인만
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
**예방:** certbot.timer 정상 작동 시 만료 30일 전 자동 갱신.
|
||||
|
||||
---
|
||||
|
||||
### PHP-FPM Pool 소진 (max_children)
|
||||
|
||||
**증상:** 응답 지연 후 502. PHP-FPM 로그에 "server reached max_children" 경고.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo grep "max_children" /var/log/php8.4-fpm.log
|
||||
ps aux | grep "php-fpm" | grep -v grep | wc -l
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo systemctl restart php8.4-fpm
|
||||
|
||||
# max_children 조정 (예: api pool 10 -> 15)
|
||||
sudo vi /etc/php/8.4/fpm/pool.d/api.conf
|
||||
sudo systemctl reload php8.4-fpm
|
||||
```
|
||||
|
||||
**예방:** 프로세스당 약 80MB. API pool: 10 x 80MB = 800MB. 메모리 여유 시만 증가.
|
||||
|
||||
---
|
||||
|
||||
### Laravel Storage 권한 문제
|
||||
|
||||
**증상:** "Permission denied". 로그 파일 작성 불가. 파일 업로드 실패.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
ls -la /home/webservice/api/shared/storage/
|
||||
ls -la /home/webservice/api/shared/storage/logs/
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:webservice /home/webservice/api/shared/storage
|
||||
sudo chmod -R 775 /home/webservice/api/shared/storage
|
||||
sudo chown -R www-data:webservice /home/webservice/api/current/bootstrap/cache
|
||||
sudo chmod -R 775 /home/webservice/api/current/bootstrap/cache
|
||||
```
|
||||
|
||||
**예방:** 배포 스크립트에 권한 설정 포함. shared/storage 심링크 확인.
|
||||
|
||||
---
|
||||
|
||||
### MNG 500 에러 (storage/logs 권한 + SOAP)
|
||||
|
||||
**증상:** mng.codebridge-x.com 특정 페이지에서 500 에러. Laravel 로그에 기록 없음.
|
||||
|
||||
**배경:** 2026-02-26 이후 MNG `storage/logs`는 shared로 심링크됨. 이전에는 릴리즈 디렉토리에 직접 생성되어 배포마다 로그가 유실되었음.
|
||||
|
||||
**진단 순서:**
|
||||
|
||||
```bash
|
||||
# 1. 로그 심링크 확인
|
||||
ls -la /home/webservice/mng/current/storage/logs
|
||||
# → shared/storage/logs 심링크인지 확인
|
||||
|
||||
# 2. 로그 파일 소유자 확인
|
||||
ls -la /home/webservice/mng/shared/storage/logs/laravel.log
|
||||
|
||||
# 3. nginx 접근 로그에서 500 확인
|
||||
sudo tail -20 /var/log/nginx/mng.codebridge-x.com.access.log | grep " 500 "
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
# 로그 심링크가 아닌 경우 (이전 배포 방식)
|
||||
rm -rf /home/webservice/mng/current/storage/logs
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/current/storage/logs
|
||||
|
||||
# shared 로그 권한 수정
|
||||
sudo chown www-data:webservice /home/webservice/mng/shared/storage/logs/
|
||||
sudo chown www-data:webservice /home/webservice/mng/shared/storage/logs/laravel.log
|
||||
|
||||
# 로그 확인
|
||||
cat /home/webservice/mng/shared/storage/logs/laravel.log
|
||||
```
|
||||
|
||||
**실제 사례 (2026-02-25):**
|
||||
|
||||
1. 최초 증상: `Table 'sam.cache' doesn't exist` → `CACHE_STORE=database`였으나 cache 테이블 미존재
|
||||
2. 해결: `.env`에서 `CACHE_STORE=redis`로 변경 + `php artisan config:cache`
|
||||
3. 여전히 500 → 로그 파일 권한 문제로 에러 미기록 → 권한 수정 후 실제 에러 확인
|
||||
4. 실제 원인: `Class "SoapClient" not found` → `php8.4-soap` 미설치
|
||||
5. 최종 해결: `sudo apt install php8.4-soap && sudo systemctl restart php8.4-fpm`
|
||||
|
||||
**교훈:**
|
||||
- MNG 로그는 `shared/storage/logs/`에 있음 (2026-02-26~)
|
||||
- 500 에러인데 로그가 비어있으면 **심링크 여부 → 파일 권한** 순서로 확인
|
||||
- PHP 확장 누락은 artisan tinker로 확인 가능: `php artisan tinker --execute="new SoapClient('test');"`
|
||||
|
||||
---
|
||||
|
||||
### MNG 전자계약(E-Sign) PDF 서명 합성 오류
|
||||
|
||||
**증상:** 전자계약 완료 후 다운로드한 PDF에 서명/도장/텍스트가 적용되지 않음. DB에서 `signed_file_path`가 null.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
# 1. 완료됐지만 signed_file_path 없는 계약 확인
|
||||
cd /home/webservice/mng/current && php artisan tinker --execute="
|
||||
\$contracts = App\Models\ESign\EsignContract::withoutGlobalScopes()
|
||||
->where('status', 'completed')->whereNull('signed_file_path')
|
||||
->get(['id','tenant_id','status','completed_at']);
|
||||
echo \$contracts->toJson(JSON_PRETTY_PRINT);
|
||||
"
|
||||
|
||||
# 2. 서명 이미지 파일 존재 확인
|
||||
sudo ls -la /home/webservice/mng/shared/storage/app/private/esign/*/signatures/
|
||||
|
||||
# 3. signed 디렉토리 존재 및 권한 확인
|
||||
ls -la /home/webservice/mng/shared/storage/app/private/esign/*/signed/
|
||||
|
||||
# 4. 로그 확인
|
||||
grep -i "서명\|esign\|pdf" /home/webservice/mng/shared/storage/logs/laravel.log | tail -20
|
||||
|
||||
# 5. 한글 폰트 확인
|
||||
ls -la /usr/share/fonts/truetype/nanum/NanumGothic.ttf
|
||||
```
|
||||
|
||||
**조치 (수동 PDF 재합성):**
|
||||
|
||||
```bash
|
||||
cd /home/webservice/mng/current && sudo -u www-data php artisan tinker --execute="
|
||||
try {
|
||||
\$contract = App\Models\ESign\EsignContract::withoutGlobalScopes()->find(<계약ID>);
|
||||
\$pdfService = new App\Services\ESign\PdfSignatureService;
|
||||
\$result = \$pdfService->mergeSignatures(\$contract);
|
||||
echo 'SUCCESS: ' . \$result;
|
||||
} catch (\Throwable \$e) {
|
||||
echo 'ERROR: ' . \$e->getMessage();
|
||||
}
|
||||
"
|
||||
```
|
||||
|
||||
**주의:** 반드시 `sudo -u www-data`로 실행해야 서명 이미지 파일 접근 가능.
|
||||
|
||||
**주요 원인 및 해결:**
|
||||
|
||||
| 원인 | 진단 방법 | 해결 |
|
||||
|------|----------|------|
|
||||
| `signed/` 디렉토리 미존재 | `ls esign/*/signed/` | `sudo -u www-data mkdir -p esign/{tenant_id}/signed` |
|
||||
| `signatures/` 권한 부족 | `stat esign/*/signatures/` | `sudo chmod 2775 esign/*/signatures/` |
|
||||
| 로그 유실로 에러 추적 불가 | `ls -la current/storage/logs` | `storage/logs` → shared 심링크 확인 |
|
||||
| 한글 폰트 미설치 | `ls /usr/share/fonts/truetype/nanum/` | `sudo apt install fonts-nanum` |
|
||||
| FPDI/TCPDF 미설치 | `composer show setasign/fpdi` | `composer install` |
|
||||
| TCPDF 폰트 정의 파일 오류 | 아래 "TCPDF 폰트 정의 파일 오류" 참고 | `registerKoreanFont()` 코드 수정 |
|
||||
|
||||
**esign 디렉토리 권한 기준:**
|
||||
|
||||
```bash
|
||||
# 모든 esign 하위 디렉토리: www-data:webservice 2775
|
||||
sudo chown -R www-data:webservice /home/webservice/mng/shared/storage/app/private/esign/
|
||||
sudo chmod -R 2775 /home/webservice/mng/shared/storage/app/private/esign/
|
||||
```
|
||||
|
||||
**실제 사례 (2026-02-26):**
|
||||
|
||||
1. 계약 #17이 `completed`인데 `signed_file_path`가 null
|
||||
2. 원인: `signatures/` 디렉토리 권한 `2700` (www-data만 접근 가능), `signed/` 디렉토리 미존재
|
||||
3. 추가 원인: `storage/logs`가 릴리즈 디렉토리에 있어 이전 배포 로그 유실
|
||||
4. 조치: 권한 `2775`로 수정 + `sudo -u www-data`로 수동 재합성 + storage/logs 심링크 적용
|
||||
5. 결과: 409KB signed PDF 생성 (원본 265KB + 서명 이미지 144KB)
|
||||
|
||||
---
|
||||
|
||||
### TCPDF 폰트 정의 파일 오류 (font definition file)
|
||||
|
||||
**증상:** 전자계약 서명 페이지에서 `TCPDF ERROR: Could not include font definition file: pretendard` (또는 `nanumgothic`) 오류.
|
||||
|
||||
**근본 원인:**
|
||||
|
||||
운영 환경에서 `vendor/tecnickcom/tcpdf/fonts/` 디렉토리가 배포 사용자(`hskwon`) 소유이므로 PHP-FPM(`www-data`)이 쓰기 불가.
|
||||
`TCPDF_FONTS::addTTFfont()`는 폰트 캐시 파일(.php, .z, .ctg.z)을 **생성만** 하고,
|
||||
`$pdf->SetFont('폰트명')`은 `K_PATH_FONTS`(vendor 경로)에서 **찾기만** 해서 경로 불일치 발생.
|
||||
|
||||
개발서버는 `vendor/` 권한이 `2775 pro:develop`이라 PHP가 직접 쓸 수 있어 문제없음.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
# 폰트 캐시 존재 확인 (storage에 있으나 vendor에 없는 상태)
|
||||
ls -la /home/webservice/mng/shared/storage/app/private/fonts/
|
||||
ls /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/pretendard* 2>/dev/null
|
||||
|
||||
# vendor fonts 소유자 확인
|
||||
stat -c "%U:%G %a" /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/
|
||||
|
||||
# 에러 로그 확인
|
||||
grep -i "font definition\|Could not include" /home/webservice/mng/shared/storage/logs/laravel.log
|
||||
```
|
||||
|
||||
**영구 해결 (코드 수정 - 2026-02-26 적용):**
|
||||
|
||||
`PdfSignatureService.php`에서 `registerKoreanFont(Fpdi $pdf)` 메서드로 분리하여:
|
||||
1. 폰트 캐시를 `storage/app/private/fonts/`에 생성 (vendor 의존 제거)
|
||||
2. `$pdf->AddFont('pretendard', '', $fontDefFile)` — PDF 인스턴스에 **전체 경로로 등록**
|
||||
3. 이후 `SetFont('pretendard')`가 이미 등록된 폰트를 사용하므로 K_PATH_FONTS 미참조
|
||||
|
||||
**긴급 임시 조치 (코드 수정 전):**
|
||||
|
||||
```bash
|
||||
# vendor 폰트 디렉토리 권한 변경 (배포 시마다 초기화됨)
|
||||
sudo chown -R www-data:webservice /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/
|
||||
sudo chmod -R 775 /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/
|
||||
|
||||
# 기존 캐시 삭제 (코드 수정 후 새 경로로 재생성)
|
||||
sudo rm -f /home/webservice/mng/shared/storage/app/private/fonts/pretendard.*
|
||||
sudo rm -f /home/webservice/mng/shared/storage/app/private/fonts/nanumgothic.*
|
||||
```
|
||||
|
||||
**개발 vs 운영 환경 차이:**
|
||||
|
||||
| 항목 | 개발 서버 | 운영 서버 |
|
||||
|------|----------|----------|
|
||||
| vendor/ 소유자 | `pro:develop` (2775) | `hskwon:hskwon` (배포 사용자) |
|
||||
| www-data vendor 쓰기 | ✅ 가능 | ❌ 불가 |
|
||||
| 폰트 캐시 위치 | vendor 내부 (기본) | storage/app/private/fonts/ |
|
||||
| `addTTFfont()` 결과 | vendor에 캐시 생성 → SetFont 성공 | storage에 캐시 생성 → SetFont 실패 (경로 불일치) |
|
||||
|
||||
---
|
||||
|
||||
## 공통 장애
|
||||
|
||||
### 디스크 공간 부족
|
||||
|
||||
**증상:** 서비스 오류. 로그 기록 실패. MySQL 쓰기 실패.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
df -h
|
||||
sudo du -sh /var/log/*
|
||||
```
|
||||
|
||||
**[운영] 정리:**
|
||||
|
||||
```bash
|
||||
cd /home/webservice/api/releases && ls -1dt */ | tail -n +4 | xargs rm -rf
|
||||
cd /home/webservice/react/releases && ls -1dt */ | tail -n +4 | xargs rm -rf
|
||||
sudo find /var/log -name "*.gz" -mtime +30 -delete
|
||||
sudo truncate -s 0 /home/webservice/api/shared/storage/logs/laravel.log
|
||||
pm2 flush
|
||||
sudo mysql -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);"
|
||||
sudo apt clean
|
||||
```
|
||||
|
||||
**[CI/CD] 정리:**
|
||||
|
||||
```bash
|
||||
sudo rm -rf /var/lib/jenkins/workspace/*
|
||||
sudo find /var/lib/jenkins/jobs/*/builds -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
|
||||
sudo journalctl --vacuum-size=500M
|
||||
find /home/hskwon/backups -name "*.sql.gz" -mtime +14 -delete
|
||||
sudo apt clean && sudo apt autoremove -y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 메모리 부족 (OOM)
|
||||
|
||||
**증상:** 프로세스 갑자기 종료. dmesg에 "Out of memory" 메시지.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
free -h
|
||||
sudo dmesg | grep -i "out of memory"
|
||||
sudo dmesg | grep -i "killed process"
|
||||
ps aux --sort=-%mem | head -15
|
||||
```
|
||||
|
||||
**[운영] 조치:**
|
||||
|
||||
```bash
|
||||
cd /home/webservice/api/current && php artisan cache:clear
|
||||
redis-cli flushall
|
||||
```
|
||||
|
||||
**[운영] 메모리 배분:** MySQL 2GB, Redis 512MB, PHP-FPM ~1.5GB, PM2 ~0.75GB, OS ~3GB
|
||||
|
||||
**[CI/CD] 조치:**
|
||||
|
||||
```bash
|
||||
# Jenkins JVM 메모리 축소 (긴급)
|
||||
# override.conf: -Xmx2048m -> -Xmx1536m
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart jenkins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 서버 접속 불가 (SSH 타임아웃)
|
||||
|
||||
**진단 (로컬에서):**
|
||||
|
||||
```bash
|
||||
ping 서버_IP
|
||||
nc -zv 서버_IP 22
|
||||
nc -zv 서버_IP 80
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
- ping 응답 없음: IDC 업체에 서버 상태 확인 요청
|
||||
- ping 응답, SSH 불가: fail2ban IP 차단 의심. IDC 콘솔 또는 다른 IP에서 접속하여 `sudo fail2ban-client set sshd unbanip 본인_IP`
|
||||
- 웹은 되나 SSH만 불가: `sudo systemctl restart sshd` (IDC 콘솔)
|
||||
|
||||
**예방:** 관리자 IP를 fail2ban whitelist에 추가.
|
||||
|
||||
---
|
||||
|
||||
### fail2ban 정상 IP 차단
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client status sshd
|
||||
sudo fail2ban-client get sshd banned | grep 차단의심_IP
|
||||
```
|
||||
|
||||
**조치:**
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client set sshd unbanip 차단된_IP주소
|
||||
sudo systemctl restart fail2ban # 전체 차단 초기화
|
||||
```
|
||||
|
||||
**예방:**
|
||||
|
||||
```bash
|
||||
# /etc/fail2ban/jail.local
|
||||
[DEFAULT]
|
||||
ignoreip = 127.0.0.1/8 관리자_IP_1 관리자_IP_2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD 서버 장애
|
||||
|
||||
### Jenkins 시작 실패
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo journalctl -u jenkins --since "10 minutes ago" --no-pager
|
||||
ps aux | grep java
|
||||
df -h
|
||||
free -h
|
||||
```
|
||||
|
||||
**(a) Java Heap 메모리 부족** (로그: `java.lang.OutOfMemoryError: Java heap space`)
|
||||
|
||||
```bash
|
||||
cat /etc/systemd/system/jenkins.service.d/override.conf
|
||||
# -Xmx 값 조정
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart jenkins
|
||||
```
|
||||
|
||||
**(b) 디스크 공간 부족** (로그: `No space left on device`)
|
||||
|
||||
```bash
|
||||
sudo rm -rf /var/lib/jenkins/workspace/*
|
||||
sudo find /var/lib/jenkins/jobs/*/builds -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
|
||||
sudo journalctl --vacuum-size=500M
|
||||
sudo systemctl restart jenkins
|
||||
```
|
||||
|
||||
**(c) 플러그인 충돌** (업데이트 후 시작 실패, ClassNotFoundException)
|
||||
|
||||
```bash
|
||||
ls -lt /var/lib/jenkins/plugins/*.jpi | head -10
|
||||
sudo rm /var/lib/jenkins/plugins/문제플러그인.jpi
|
||||
sudo systemctl restart jenkins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Jenkins 빌드 실패
|
||||
|
||||
**(a) npm/composer 오류:**
|
||||
|
||||
```bash
|
||||
sudo -u jenkins npm cache clean --force
|
||||
sudo rm -rf /var/lib/jenkins/workspace/<JOB>/node_modules
|
||||
```
|
||||
|
||||
**(b) SSH 키 문제:** (`Permission denied`, `Host key verification failed`)
|
||||
|
||||
```bash
|
||||
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519 hskwon@211.117.60.189 "echo OK"
|
||||
sudo -u jenkins ssh-keyscan -H 211.117.60.189 >> /var/lib/jenkins/.ssh/known_hosts
|
||||
sudo -u jenkins ssh-keyscan -H 114.203.209.83 >> /var/lib/jenkins/.ssh/known_hosts
|
||||
```
|
||||
|
||||
**(c) rsync 실패:** (`connection unexpectedly closed`)
|
||||
|
||||
```bash
|
||||
ssh sam-prod "df -h"
|
||||
ssh sam-prod "ls -la /home/webservice/react/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Gitea 접속 불가
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status gitea
|
||||
curl -I http://localhost:3000
|
||||
sudo ss -tlnp | grep 3000
|
||||
```
|
||||
|
||||
**(a) 포트 충돌:**
|
||||
|
||||
```bash
|
||||
sudo fuser 3000/tcp
|
||||
sudo systemctl restart gitea
|
||||
```
|
||||
|
||||
**(b) DB 연결 실패:**
|
||||
|
||||
```bash
|
||||
sudo systemctl status mysql
|
||||
mysql -u gitea -p gitea -e "SELECT 1;"
|
||||
sudo systemctl restart mysql && sudo systemctl restart gitea
|
||||
```
|
||||
|
||||
**(c) 설정 파일 오류:**
|
||||
|
||||
```bash
|
||||
sudo chown git:git /etc/gitea/app.ini
|
||||
sudo systemctl restart gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Gitea push/pull 느림
|
||||
|
||||
```bash
|
||||
sudo tail -50 /var/lib/gitea/log/gitea.log
|
||||
sudo du -sh /var/lib/gitea/data/repositories/SamProject/*
|
||||
|
||||
# Git GC (저장소 최적화)
|
||||
sudo -u git git -C /var/lib/gitea/data/repositories/SamProject/sam-react-prod.git gc --aggressive
|
||||
sudo systemctl restart gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Prometheus 스크래핑 실패
|
||||
|
||||
**증상:** Grafana에서 데이터 없음.
|
||||
|
||||
```bash
|
||||
sudo systemctl status prometheus
|
||||
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep -A5 "health"
|
||||
promtool check config /etc/prometheus/prometheus.yml
|
||||
|
||||
# 대상 서버 연결 확인
|
||||
curl -s --connect-timeout 5 http://211.117.60.189:9100/metrics | head -5
|
||||
ssh sam-prod "sudo ufw status | grep 9100"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Grafana 대시보드 로딩 실패
|
||||
|
||||
```bash
|
||||
sudo systemctl status grafana-server
|
||||
curl -I http://localhost:3100
|
||||
sudo systemctl restart grafana-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 긴급 연락처
|
||||
|
||||
| 역할 | 연락처 | 비고 |
|
||||
|------|--------|------|
|
||||
| 서버 관리 | hskwon | SSH 접속 가능 |
|
||||
| IDC 업체 | (확인 후 기입 필요) | 서버 물리적 장애, 네트워크 |
|
||||
244
dev/deploys/ops-manual/09-security.md
Normal file
244
dev/deploys/ops-manual/09-security.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# 9. 보안 관리
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## SSH 키 관리
|
||||
|
||||
양쪽 서버 모두 비밀번호 로그인 비활성화, root SSH 비활성화, 키 인증만 허용.
|
||||
|
||||
```bash
|
||||
# SSH 설정 확인
|
||||
sudo grep -E "^(PasswordAuthentication|PermitRootLogin|PubkeyAuthentication)" /etc/ssh/sshd_config
|
||||
# 올바른 설정:
|
||||
# PasswordAuthentication no
|
||||
# PermitRootLogin no
|
||||
# PubkeyAuthentication yes
|
||||
```
|
||||
|
||||
### [운영] 공개키 관리
|
||||
|
||||
```bash
|
||||
cat /home/hskwon/.ssh/authorized_keys
|
||||
|
||||
# 새 공개키 추가
|
||||
echo "새_공개키_내용" >> /home/hskwon/.ssh/authorized_keys
|
||||
|
||||
# SSH 설정 변경 후 반드시 재시작
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
### [CI/CD] 공개키 관리
|
||||
|
||||
```bash
|
||||
cat /home/hskwon/.ssh/authorized_keys
|
||||
echo "ssh-ed25519 AAAA... user@host" >> /home/hskwon/.ssh/authorized_keys
|
||||
chmod 600 /home/hskwon/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
### [CI/CD] Jenkins SSH 키
|
||||
|
||||
```bash
|
||||
# 경로: /var/lib/jenkins/.ssh/id_ed25519
|
||||
# 공개키는 운영서버/개발서버 hskwon authorized_keys에 등록됨
|
||||
sudo cat /var/lib/jenkins/.ssh/id_ed25519.pub
|
||||
|
||||
# 연결 테스트
|
||||
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519 hskwon@211.117.60.189 "hostname && date"
|
||||
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519 hskwon@114.203.209.83 "hostname && date"
|
||||
|
||||
# known_hosts 갱신 (호스트 키 변경 시)
|
||||
sudo -u jenkins ssh-keygen -R 211.117.60.189
|
||||
sudo -u jenkins ssh-keyscan -H 211.117.60.189 >> /var/lib/jenkins/.ssh/known_hosts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UFW (방화벽) 관리
|
||||
|
||||
### [운영] 규칙
|
||||
|
||||
| 포트 | 허용 범위 | 용도 |
|
||||
|------|-----------|------|
|
||||
| 22 | Anywhere | SSH |
|
||||
| 80 | Anywhere | HTTP |
|
||||
| 443 | Anywhere | HTTPS |
|
||||
| 9100 | 110.10.147.46 only | node_exporter |
|
||||
| 3306 | 110.10.147.46 only | MySQL 백업 |
|
||||
|
||||
### [CI/CD] 규칙
|
||||
|
||||
| 포트 | 허용 범위 | 용도 |
|
||||
|------|-----------|------|
|
||||
| 22 | Anywhere | SSH |
|
||||
| 80 | Anywhere | HTTP |
|
||||
| 443 | Anywhere | HTTPS |
|
||||
|
||||
### [개발] 규칙
|
||||
|
||||
| 포트 | 허용 범위 | 용도 |
|
||||
|------|-----------|------|
|
||||
| 22 | Anywhere | SSH |
|
||||
| 80 | Anywhere | HTTP |
|
||||
| 443 | Anywhere | HTTPS |
|
||||
| 3000 | Anywhere | Gitea |
|
||||
|
||||
> MySQL(3306), Apache(8080), Next.js(3001) 등은 외부 차단됨
|
||||
|
||||
### 공통 명령어
|
||||
|
||||
```bash
|
||||
# 규칙 확인
|
||||
sudo ufw status numbered
|
||||
|
||||
# 규칙 추가
|
||||
sudo ufw allow from IP주소 to any port 포트번호
|
||||
|
||||
# 규칙 삭제
|
||||
sudo ufw delete 규칙_번호
|
||||
|
||||
# 변경사항은 즉시 적용 (재시작 불필요)
|
||||
```
|
||||
|
||||
**주의:** SSH (22/tcp) 규칙 삭제 금지
|
||||
|
||||
```bash
|
||||
# 변경 전 백업 (CI/CD)
|
||||
sudo ufw status numbered > /tmp/ufw-backup-$(date +%Y%m%d).txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSL 인증서 관리
|
||||
|
||||
```bash
|
||||
# 인증서 만료일 전체 확인
|
||||
sudo certbot certificates
|
||||
|
||||
# 자동 갱신 타이머 확인
|
||||
sudo systemctl status certbot.timer
|
||||
|
||||
# 새 도메인 인증서 발급
|
||||
sudo certbot --nginx -d 새도메인 --email develop@codebridge-x.com
|
||||
|
||||
# 수동 갱신
|
||||
sudo certbot renew
|
||||
|
||||
# 인증서 삭제
|
||||
sudo certbot delete --cert-name 도메인명
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## fail2ban 관리
|
||||
|
||||
```bash
|
||||
# jail 상태 확인
|
||||
sudo fail2ban-client status
|
||||
sudo fail2ban-client status sshd
|
||||
|
||||
# IP 차단 해제
|
||||
sudo fail2ban-client set sshd unbanip IP주소
|
||||
|
||||
# jail 재시작
|
||||
sudo fail2ban-client restart sshd
|
||||
```
|
||||
|
||||
### 화이트리스트 설정
|
||||
|
||||
**현재 설정:**
|
||||
|
||||
| 서버 | ignoreip |
|
||||
|------|----------|
|
||||
| 운영 | 127.0.0.1/8, 110.10.147.46 (CI/CD) |
|
||||
| CI/CD | 127.0.0.1/8, 211.117.60.189 (운영) |
|
||||
| 개발 | 127.0.0.1/8, 110.10.147.46 (CI/CD), 211.117.60.189 (운영) |
|
||||
|
||||
```bash
|
||||
# /etc/fail2ban/jail.local
|
||||
[DEFAULT]
|
||||
ignoreip = 127.0.0.1/8 110.10.147.46 211.117.60.189
|
||||
|
||||
# 변경 후
|
||||
sudo systemctl restart fail2ban
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [운영] .env 파일 보안
|
||||
|
||||
> **주의:** `.env` 권한은 반드시 `640` (`-rw-r-----`)이어야 합니다.
|
||||
> PHP-FPM은 `www-data` 사용자(webservice 그룹)로 실행되므로 그룹 읽기 권한이 필요합니다.
|
||||
> `600`으로 설정하면 PHP-FPM이 .env를 읽지 못해 **전체 서비스 500 에러**가 발생합니다.
|
||||
> (실제 장애 사례: 2026-03-03, 08-troubleshooting.md 참조)
|
||||
|
||||
```bash
|
||||
# 권한 확인 (640이어야 함 — 소유자 rw + 그룹 r)
|
||||
ls -la /home/webservice/api/shared/.env
|
||||
ls -la /home/webservice/mng/shared/.env
|
||||
ls -la /home/webservice/sales/.env
|
||||
|
||||
# 권한 수정
|
||||
chmod 640 /home/webservice/api/shared/.env
|
||||
chmod 640 /home/webservice/mng/shared/.env
|
||||
chmod 640 /home/webservice/sales/.env
|
||||
```
|
||||
|
||||
### vi 편집 시 권한 변경 방지
|
||||
|
||||
`vi`로 파일을 편집하면 새 파일로 교체되면서 권한이 `600`으로 초기화될 수 있습니다.
|
||||
이를 방지하기 위해 서버 계정의 `~/.vimrc`에 아래 설정을 추가합니다:
|
||||
|
||||
```bash
|
||||
# 원본 파일에 직접 덮어쓰기 (권한 유지)
|
||||
echo "set backupcopy=yes" >> ~/.vimrc
|
||||
```
|
||||
|
||||
**적용 현황 (2026-03-03):**
|
||||
- sam-prod: hskwon, pro 계정 적용 완료
|
||||
- sam-cicd: hskwon, pro 계정 적용 완료
|
||||
|
||||
---
|
||||
|
||||
## [운영] Redis 보안
|
||||
|
||||
Redis는 127.0.0.1에만 바인딩되어 외부 접근 불가.
|
||||
|
||||
```bash
|
||||
redis-cli config get bind # "127.0.0.1 ::1"
|
||||
grep "^bind" /etc/redis/redis.conf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [운영] MySQL 사용자 관리
|
||||
|
||||
```bash
|
||||
# 사용자 목록
|
||||
sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"
|
||||
|
||||
# 비밀번호 변경
|
||||
sudo mysql -e "ALTER USER 'codebridge'@'localhost' IDENTIFIED BY '새_비밀번호'; FLUSH PRIVILEGES;"
|
||||
|
||||
# 외부 접근 사용자 확인
|
||||
sudo mysql -e "SELECT user, host FROM mysql.user WHERE host != 'localhost';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Jenkins 보안
|
||||
|
||||
- Jenkins Credentials에서만 민감 정보 관리
|
||||
- Jenkinsfile에 직접 비밀번호 기재 금지
|
||||
- 관리자: hskwon
|
||||
- 사용자 추가: Jenkins 관리 > Users > Create User
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Gitea 접근 제어
|
||||
|
||||
- 회원가입 비활성화 (DISABLE_REGISTRATION = true)
|
||||
- 로그인 필수 (REQUIRE_SIGNIN_VIEW = true)
|
||||
- API 토큰 기반 인증
|
||||
- 사용자 추가: CLI 또는 관리자 웹 UI
|
||||
632
dev/deploys/ops-manual/10-backup-recovery.md
Normal file
632
dev/deploys/ops-manual/10-backup-recovery.md
Normal file
@@ -0,0 +1,632 @@
|
||||
# 10. 백업, 복구, 재부팅
|
||||
|
||||
[목차로 돌아가기](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## [운영] DB 백업
|
||||
|
||||
### 수동 백업
|
||||
|
||||
```bash
|
||||
# sam DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# sam_stat DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers sam_stat | gzip > /tmp/sam_stat_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# codebridge DB (Sales)
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
|
||||
# 전체 DB
|
||||
mysqldump -u hskwon --single-transaction --routines --triggers --all-databases | gzip > /tmp/all_db_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
```
|
||||
|
||||
### 파일 백업 (업로드, Storage)
|
||||
|
||||
```bash
|
||||
# API storage
|
||||
tar czf /tmp/api_storage_$(date +%Y%m%d).tar.gz -C /home/webservice/api/shared storage
|
||||
|
||||
# MNG storage
|
||||
tar czf /tmp/mng_storage_$(date +%Y%m%d).tar.gz -C /home/webservice/mng/shared storage
|
||||
|
||||
# Sales uploads
|
||||
tar czf /tmp/sales_uploads_$(date +%Y%m%d).tar.gz -C /home/webservice/sales uploads
|
||||
|
||||
# 외부 전송
|
||||
scp /tmp/*_$(date +%Y%m%d).tar.gz sam-cicd:/home/hskwon/backups/files/
|
||||
```
|
||||
|
||||
### .env 백업
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/env_backup
|
||||
cp /home/webservice/api/shared/.env /tmp/env_backup/api.env
|
||||
cp /home/webservice/mng/shared/.env /tmp/env_backup/mng.env
|
||||
cp /home/webservice/sales/.env /tmp/env_backup/sales.env
|
||||
|
||||
tar czf /tmp/env_backup_$(date +%Y%m%d).tar.gz -C /tmp env_backup
|
||||
scp /tmp/env_backup_$(date +%Y%m%d).tar.gz sam-cicd:/home/hskwon/backups/env/
|
||||
rm -rf /tmp/env_backup /tmp/env_backup_*.tar.gz
|
||||
```
|
||||
|
||||
### DB 복구
|
||||
|
||||
```bash
|
||||
# 전체 DB 복구
|
||||
gunzip -c /path/to/sam_백업파일.sql.gz | sudo mysql sam
|
||||
|
||||
# 특정 테이블
|
||||
sudo mysql sam < /path/to/sam_테이블명_백업파일.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Gitea 백업/복구
|
||||
|
||||
### 백업
|
||||
|
||||
```bash
|
||||
# 전체 백업 (저장소 + DB + 설정)
|
||||
sudo mkdir -p /home/hskwon/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
|
||||
|
||||
# 저장소만
|
||||
sudo tar czf /home/hskwon/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
|
||||
```
|
||||
|
||||
### 복구
|
||||
|
||||
```bash
|
||||
sudo systemctl stop gitea
|
||||
|
||||
cd /tmp
|
||||
unzip /home/hskwon/backups/gitea/gitea-dump-YYYYMMDD.zip
|
||||
|
||||
mysql -u root gitea < gitea-db.sql
|
||||
sudo rsync -av gitea-repo/ /var/lib/gitea/data/repositories/
|
||||
sudo chown -R git:git /var/lib/gitea/data/repositories/
|
||||
sudo cp app.ini /etc/gitea/app.ini
|
||||
sudo chown git:git /etc/gitea/app.ini
|
||||
|
||||
sudo systemctl start gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Jenkins 백업/복구
|
||||
|
||||
### 백업
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /home/hskwon/backups/jenkins
|
||||
|
||||
# Jobs 설정
|
||||
sudo tar czf /home/hskwon/backups/jenkins/jobs-$(date +%Y%m%d).tar.gz \
|
||||
-C /var/lib/jenkins jobs/
|
||||
|
||||
# Credentials
|
||||
sudo tar czf /home/hskwon/backups/jenkins/secrets-$(date +%Y%m%d).tar.gz \
|
||||
-C /var/lib/jenkins secrets/ credentials.xml
|
||||
|
||||
# 플러그인 목록
|
||||
sudo ls /var/lib/jenkins/plugins/*.jpi 2>/dev/null | xargs -I{} basename {} .jpi \
|
||||
> /home/hskwon/backups/jenkins/plugins-$(date +%Y%m%d).txt
|
||||
|
||||
# 환경변수 파일
|
||||
sudo tar czf /home/hskwon/backups/jenkins/env-files-$(date +%Y%m%d).tar.gz \
|
||||
-C /var/lib/jenkins env-files/
|
||||
|
||||
# SSH 키
|
||||
sudo tar czf /home/hskwon/backups/jenkins/ssh-$(date +%Y%m%d).tar.gz \
|
||||
-C /var/lib/jenkins .ssh/
|
||||
```
|
||||
|
||||
### 복구
|
||||
|
||||
```bash
|
||||
sudo systemctl stop jenkins
|
||||
sudo tar xzf /home/hskwon/backups/jenkins/jobs-YYYYMMDD.tar.gz -C /var/lib/jenkins/
|
||||
sudo tar xzf /home/hskwon/backups/jenkins/secrets-YYYYMMDD.tar.gz -C /var/lib/jenkins/
|
||||
sudo tar xzf /home/hskwon/backups/jenkins/env-files-YYYYMMDD.tar.gz -C /var/lib/jenkins/
|
||||
sudo tar xzf /home/hskwon/backups/jenkins/ssh-YYYYMMDD.tar.gz -C /var/lib/jenkins/
|
||||
sudo chown -R jenkins:jenkins /var/lib/jenkins/
|
||||
sudo systemctl start jenkins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Prometheus / Grafana 백업
|
||||
|
||||
```bash
|
||||
# Prometheus 설정 (필수)
|
||||
sudo cp /etc/prometheus/prometheus.yml /home/hskwon/backups/prometheus-config-$(date +%Y%m%d).yml
|
||||
|
||||
# Prometheus 데이터 (선택, 보존 기간 30일)
|
||||
sudo systemctl stop prometheus
|
||||
sudo tar czf /home/hskwon/backups/prometheus-data-$(date +%Y%m%d).tar.gz /var/lib/prometheus/
|
||||
sudo systemctl start prometheus
|
||||
|
||||
# Grafana 설정 + 대시보드
|
||||
sudo mkdir -p /home/hskwon/backups/grafana
|
||||
sudo cp /etc/grafana/grafana.ini /home/hskwon/backups/grafana/grafana.ini-$(date +%Y%m%d)
|
||||
sudo tar czf /home/hskwon/backups/grafana/grafana-data-$(date +%Y%m%d).tar.gz /var/lib/grafana/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] MySQL 자동 백업 (운영 DB)
|
||||
|
||||
### 개요
|
||||
|
||||
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/` |
|
||||
| 보존 기간 | **14일** (자동 삭제) |
|
||||
| 로그 | `/home/hskwon/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 테이블) |
|
||||
|
||||
### 백업 스크립트
|
||||
|
||||
```bash
|
||||
# /home/hskwon/scripts/backup-db.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/home/hskwon/backups/mysql"
|
||||
BACKUP_CNF="/home/hskwon/.sam_backup.cnf"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
RETENTION_DAYS=14
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Gitea DB 백업 (로컬, auth_socket)
|
||||
mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/gitea_$DATE.sql.gz
|
||||
|
||||
# 운영 DB 원격 백업 (sam_backup 사용자)
|
||||
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
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log
|
||||
fi
|
||||
|
||||
# 오래된 백업 삭제
|
||||
find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
```
|
||||
|
||||
### 인증 설정
|
||||
|
||||
```ini
|
||||
# /home/hskwon/.sam_backup.cnf (chmod 600)
|
||||
[client]
|
||||
user=sam_backup
|
||||
password=<백업용_비밀번호>
|
||||
```
|
||||
|
||||
### 크론탭 (sam-cicd 서버, hskwon 유저)
|
||||
|
||||
```crontab
|
||||
# SAM DB 백업 (매일 새벽 3시)
|
||||
0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1
|
||||
```
|
||||
|
||||
### 수동 실행 및 확인
|
||||
|
||||
```bash
|
||||
# 수동 백업 실행
|
||||
/home/hskwon/scripts/backup-db.sh
|
||||
|
||||
# 백업 파일 확인
|
||||
ls -lht /home/hskwon/backups/mysql/
|
||||
|
||||
# 백업 로그 확인
|
||||
tail -10 /home/hskwon/backups/mysql/backup.log
|
||||
|
||||
# 크론 스케줄 확인
|
||||
crontab -l
|
||||
```
|
||||
|
||||
### 백업 복원 (CI/CD → 운영)
|
||||
|
||||
```bash
|
||||
# sam DB 복원 (운영 서버에서 실행)
|
||||
gunzip -c /path/to/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam
|
||||
|
||||
# sam_stat DB 복원
|
||||
gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat
|
||||
```
|
||||
|
||||
### 운영 MySQL 백업 사용자 (운영 서버 설정)
|
||||
|
||||
```sql
|
||||
-- 운영 서버(sam-prod)에서 실행
|
||||
CREATE USER 'sam_backup'@'110.10.147.46' IDENTIFIED BY '<백업용_비밀번호>';
|
||||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam.* TO 'sam_backup'@'110.10.147.46';
|
||||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam_stat.* TO 'sam_backup'@'110.10.147.46';
|
||||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON codebridge.* TO 'sam_backup'@'110.10.147.46';
|
||||
GRANT REPLICATION SLAVE ON *.* TO 'sam_backup'@'110.10.147.46';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
UFW에서 CI/CD IP의 MySQL 접근이 허용되어 있어야 합니다:
|
||||
|
||||
```bash
|
||||
# 운영 서버 UFW 규칙 확인
|
||||
sudo ufw status | grep 3306
|
||||
# → 110.10.147.46 ALLOW (CI/CD 백업/리플리케이션용)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] MySQL 리플리케이션 (운영 → CI/CD)
|
||||
|
||||
### 개요
|
||||
|
||||
운영 DB의 변경사항을 실시간으로 CI/CD 서버에 동기화합니다. binlog 기반 리플리케이션으로 변경분만 전송되어 네트워크/디스크 부하가 최소화됩니다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 방식 | **MySQL Replication** (Source → Replica) |
|
||||
| Source (운영) | 211.117.60.189, server-id=1 |
|
||||
| Replica (CI/CD) | 110.10.147.46, server-id=2 |
|
||||
| 인증 | `sam_backup@110.10.147.46` (REPLICATION SLAVE) |
|
||||
| 대상 DB | **sam**, **sam_stat**, **codebridge** |
|
||||
| 제외 DB | gitea (CI/CD 자체 DB, 리플리케이션 영향 없음) |
|
||||
| 동기화 | 실시간 (Seconds_Behind_Source ≈ 0) |
|
||||
| CI/CD MySQL | **read_only=OFF** (Gitea DB 쓰기 필요, replicate-do-db로 대상 DB 제한) |
|
||||
|
||||
### 아키텍처
|
||||
|
||||
```
|
||||
[운영 서버 211.117.60.189] [CI/CD 서버 110.10.147.46]
|
||||
MySQL (Source, server-id=1) MySQL (Replica, server-id=2)
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ sam │ ── binlog ──────────▶ │ sam │ (read-only)
|
||||
│ sam_stat│ ── binlog ──────────▶ │ sam_stat│ (read-only)
|
||||
│codebridge│── binlog ──────────▶ │codebridge│(read-only)
|
||||
└─────────┘ ├─────────┤
|
||||
│ gitea │ (독립, read-write)
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### CI/CD MySQL 설정
|
||||
|
||||
```ini
|
||||
# /etc/mysql/mysql.conf.d/sam-tuning.cnf (리플리케이션 관련 부분)
|
||||
[mysqld]
|
||||
server-id = 2
|
||||
relay-log = /var/log/mysql/mysql-relay-bin
|
||||
# read-only = 1 # Gitea DB 쓰기 필요하여 비활성화 (replicate-do-db로 대상 제한)
|
||||
replicate-do-db = sam
|
||||
replicate-do-db = sam_stat
|
||||
replicate-do-db = codebridge
|
||||
```
|
||||
|
||||
### 리플리케이션 상태 확인
|
||||
|
||||
```bash
|
||||
# CI/CD 서버(sam-cicd)에서 실행
|
||||
mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|SQL_Running|Behind|Error'
|
||||
|
||||
# 정상 상태:
|
||||
# Replica_IO_Running: Yes
|
||||
# Replica_SQL_Running: Yes
|
||||
# Seconds_Behind_Source: 0
|
||||
# Last_IO_Error: (빈 값)
|
||||
# Last_SQL_Error: (빈 값)
|
||||
```
|
||||
|
||||
### 리플리케이션 장애 복구
|
||||
|
||||
#### IO 스레드 중단 시
|
||||
|
||||
```bash
|
||||
# 에러 확인
|
||||
mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|IO_Error'
|
||||
|
||||
# 네트워크 문제: 자동 재연결 (Connect_Retry=60, 10회 시도)
|
||||
# 인증 문제: 운영 서버 sam_backup 유저 확인
|
||||
# 수동 재시작
|
||||
mysql -u hskwon -p -e "STOP REPLICA IO_THREAD; START REPLICA IO_THREAD;"
|
||||
```
|
||||
|
||||
#### SQL 스레드 에러 시
|
||||
|
||||
```bash
|
||||
# 에러 확인
|
||||
mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'SQL_Running|SQL_Error'
|
||||
|
||||
# 특정 에러 건너뛰기 (주의: 데이터 불일치 가능)
|
||||
mysql -u hskwon -p -e "SET GLOBAL SQL_REPLICA_SKIP_COUNTER = 1; START REPLICA;"
|
||||
```
|
||||
|
||||
#### 전체 재구축 (데이터 불일치 심각 시)
|
||||
|
||||
```bash
|
||||
# 1. CI/CD 리플리케이션 중지
|
||||
mysql -u hskwon -p -e "STOP REPLICA;"
|
||||
|
||||
# 2. 운영에서 새 덤프 생성
|
||||
ssh sam-prod "mysqldump -u hskwon -p --databases sam sam_stat codebridge \
|
||||
--source-data=1 --single-transaction --routines --triggers --events \
|
||||
--set-gtid-purged=OFF | gzip > /tmp/repl_rebuild.sql.gz"
|
||||
|
||||
# 3. CI/CD로 전송
|
||||
scp sam-prod:/tmp/repl_rebuild.sql.gz /tmp/
|
||||
|
||||
# 4. CI/CD에서 임포트
|
||||
zcat /tmp/repl_rebuild.sql.gz | mysql -u hskwon -p
|
||||
|
||||
# 5. 덤프 헤더에서 binlog position 확인
|
||||
zcat /tmp/repl_rebuild.sql.gz | head -30 | grep "CHANGE"
|
||||
|
||||
# 6. 리플리케이션 재설정 (position 값은 위 결과로 교체)
|
||||
mysql -u hskwon -p << 'SQL'
|
||||
CHANGE REPLICATION SOURCE TO
|
||||
SOURCE_HOST='211.117.60.189',
|
||||
SOURCE_USER='sam_backup',
|
||||
SOURCE_PASSWORD='<백업용_비밀번호>',
|
||||
SOURCE_LOG_FILE='binlog.XXXXXX',
|
||||
SOURCE_LOG_POS=XXXXXXXXX,
|
||||
GET_SOURCE_PUBLIC_KEY=1;
|
||||
START REPLICA;
|
||||
SQL
|
||||
|
||||
# 7. 임시 파일 정리
|
||||
ssh sam-prod "rm -f /tmp/repl_rebuild.sql.gz"
|
||||
rm -f /tmp/repl_rebuild.sql.gz
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- CI/CD MySQL은 `read_only=OFF` (Gitea가 같은 MySQL 사용하여 쓰기 필요) → **CI/CD에서 sam/sam_stat/codebridge DB에 직접 쓰기 금지** (replicate-do-db 필터로 리플리케이션 대상만 제한)
|
||||
- `replicate-do-db` 필터로 gitea DB는 리플리케이션 영향 없음
|
||||
- 운영 서버 MySQL 8.4는 `caching_sha2_password` 사용 → 리플리케이션 설정 시 `GET_SOURCE_PUBLIC_KEY=1` 필수
|
||||
- binlog 보존 기간(`binlog_expire_logs_seconds`) 내에 리플리케이션 장애를 복구해야 함, 초과 시 전체 재구축 필요
|
||||
|
||||
---
|
||||
|
||||
## [운영] sam → sam_stage 동기화
|
||||
|
||||
Stage 환경(stage-api.sam.it.kr)은 `sam_stage` DB를 사용합니다. 운영 `sam` DB와 **자동 동기화는 없으며**, 필요 시 수동으로 동기화합니다.
|
||||
|
||||
### 스키마만 동기화 (테이블 구조)
|
||||
|
||||
운영 DB에 테이블/컬럼 변경이 있을 때 실행합니다.
|
||||
|
||||
```bash
|
||||
# 운영 서버(sam-prod)에서 실행
|
||||
# 1. sam_stage 초기화
|
||||
mysql -ucodebridge -p -e "DROP DATABASE IF EXISTS sam_stage; CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# 2. 스키마 복사 (구조만, 데이터 없음)
|
||||
mysqldump -ucodebridge -p --single-transaction --no-data --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage
|
||||
|
||||
# 3. 데이터 복사 (필요시)
|
||||
mysqldump -ucodebridge -p --single-transaction --no-create-info --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage
|
||||
|
||||
# 4. Laravel 캐시 갱신
|
||||
cd /home/webservice/api-stage/current
|
||||
php artisan config:cache
|
||||
php artisan cache:clear
|
||||
```
|
||||
|
||||
### 전체 동기화 (스키마 + 데이터)
|
||||
|
||||
Stage 환경을 운영과 동일한 상태로 리셋할 때 실행합니다.
|
||||
|
||||
```bash
|
||||
# 운영 서버(sam-prod)에서 실행
|
||||
mysql -ucodebridge -p -e "DROP DATABASE IF EXISTS sam_stage; CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
mysqldump -ucodebridge -p --single-transaction --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage
|
||||
cd /home/webservice/api-stage/current && php artisan config:cache && php artisan cache:clear
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- `codebridge` 사용자에 `SUPER`, `PROCESS` 권한이 없으므로 `--no-tablespaces --skip-triggers --skip-routines` 옵션 필수
|
||||
- sam_stage의 `.env`는 별도 관리 (`APP_URL=https://stage-api.sam.it.kr`, `APP_ENV=staging`)
|
||||
- Jenkins 파이프라인(api)의 Stage 배포 시 `php artisan migrate --force`로 스키마 자동 반영
|
||||
|
||||
---
|
||||
|
||||
## 전체 서버 복구 절차
|
||||
|
||||
### [운영] 복구 순서
|
||||
|
||||
1. OS 설치: Ubuntu 24.04 + 기본 패키지
|
||||
2. 보안 설정: SSH 키, UFW, fail2ban
|
||||
3. MySQL 복구: MySQL 8.4 설치 -> 백업 파일 복원 -> 사용자 재생성
|
||||
4. Redis 설치: Redis 7.x + 설정
|
||||
5. PHP-FPM 설치: PHP 8.4 + 확장 + Pool 설정 복원
|
||||
6. Nginx 설치: Nginx + 사이트 설정 복원 + SSL 재발급
|
||||
7. Node.js + PM2 설치: Node.js 22 + PM2
|
||||
8. 애플리케이션 배포: 각 서비스 코드 + .env 복원 + storage 복원
|
||||
9. Supervisor 설치: Queue Worker 설정
|
||||
10. 모니터링: node_exporter 설치
|
||||
|
||||
상세: [서버 설치 가이드](./11-server-setup.md)
|
||||
|
||||
### [CI/CD] 복구 순서
|
||||
|
||||
1. OS 기본 셋팅 (UFW, 스왑, 타임존)
|
||||
2. MySQL 설치 + Gitea DB 복원
|
||||
3. **MySQL 리플리케이션 설정** (sam-tuning.cnf 복원, 운영 DB 덤프 임포트, CHANGE REPLICATION SOURCE)
|
||||
4. Java 설치
|
||||
5. Gitea 설치 + 설정/저장소 복원
|
||||
6. Jenkins 설치 + jobs/credentials/env-files/SSH 키 복원
|
||||
7. Nginx 설치 + 사이트 설정 + SSL 인증서 발급
|
||||
8. Prometheus + node_exporter 설치 + 설정 복원
|
||||
9. Grafana 설치 + 대시보드 임포트
|
||||
10. fail2ban 설치
|
||||
11. Webhook 연결 확인
|
||||
12. 전체 서비스 동작 검증 (리플리케이션 상태 포함)
|
||||
|
||||
상세: [서버 설치 가이드](./11-server-setup.md)
|
||||
|
||||
---
|
||||
|
||||
## 서버 재부팅 절차
|
||||
|
||||
### [운영] 재부팅
|
||||
|
||||
**재부팅 전 점검:**
|
||||
|
||||
```bash
|
||||
# 서비스 상태 기록
|
||||
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter
|
||||
pm2 status
|
||||
|
||||
# 대기 중인 Queue 작업 확인
|
||||
cd /home/webservice/api/current && php artisan queue:monitor redis:default
|
||||
|
||||
# 진행 중인 MySQL 쿼리 확인
|
||||
sudo mysql -e "SHOW PROCESSLIST;" | grep -v Sleep
|
||||
|
||||
# PM2 상태 저장
|
||||
pm2 save
|
||||
|
||||
# 리소스 상태 기록
|
||||
free -h
|
||||
df -h
|
||||
```
|
||||
|
||||
**재부팅 실행:** `sudo reboot`
|
||||
|
||||
**재부팅 후 확인 (1~2분 후):**
|
||||
|
||||
```bash
|
||||
# 시스템 상태
|
||||
uptime && free -h && df -h
|
||||
|
||||
# 서비스 확인
|
||||
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter certbot.timer fail2ban
|
||||
pm2 status
|
||||
|
||||
# 포트 확인
|
||||
sudo ss -tlnp
|
||||
|
||||
# 웹 서비스 응답
|
||||
curl -sI https://sam.it.kr
|
||||
curl -sI https://api.sam.it.kr
|
||||
curl -sI https://mng.codebridge-x.com
|
||||
curl -sI https://sales.codebridge-x.com
|
||||
curl -sI https://stage.sam.it.kr
|
||||
curl -sI https://stage-api.sam.it.kr
|
||||
|
||||
# Redis / MySQL 연결
|
||||
redis-cli ping
|
||||
sudo mysql -e "SELECT 1;"
|
||||
|
||||
# Queue Worker
|
||||
sudo supervisorctl status
|
||||
|
||||
# 방화벽
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
**서비스 자동 시작 실패 시:**
|
||||
|
||||
```bash
|
||||
sudo systemctl start nginx
|
||||
sudo systemctl start php8.4-fpm
|
||||
sudo systemctl start mysql
|
||||
sudo systemctl start redis-server
|
||||
sudo systemctl start supervisor
|
||||
sudo systemctl start node_exporter
|
||||
sudo systemctl start fail2ban
|
||||
pm2 resurrect # 저장된 프로세스 복구
|
||||
# PM2 복구 실패 시
|
||||
cd /home/webservice && pm2 start ecosystem.config.js && pm2 save
|
||||
```
|
||||
|
||||
**자동 시작 등록 확인:**
|
||||
|
||||
```bash
|
||||
sudo systemctl is-enabled nginx php8.4-fpm mysql redis-server supervisor node_exporter fail2ban
|
||||
# 등록 안 된 서비스: sudo systemctl enable 서비스명
|
||||
# PM2는 pm2 startup + pm2 save로 관리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [CI/CD] 재부팅
|
||||
|
||||
**재부팅 전 점검:**
|
||||
|
||||
```bash
|
||||
# Jenkins 실행 중인 빌드 확인 (웹 UI: https://ci.sam.it.kr)
|
||||
|
||||
# Gitea 진행 중인 push 확인
|
||||
sudo tail -5 /var/lib/gitea/log/gitea.log
|
||||
|
||||
# 서비스 상태 기록
|
||||
sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter > /tmp/pre-reboot-status.txt
|
||||
```
|
||||
|
||||
**재부팅 실행:** `sudo reboot`
|
||||
|
||||
**재부팅 후 검증:**
|
||||
|
||||
```bash
|
||||
# 서비스 상태
|
||||
sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter
|
||||
|
||||
# 포트 확인
|
||||
sudo ss -tlnp | grep -E '(80|443|3000|3100|8080|9090|9100|3306)'
|
||||
|
||||
# 웹 서비스 응답
|
||||
curl -sI https://ci.sam.it.kr | head -3
|
||||
curl -sI https://git.sam.it.kr | head -3
|
||||
curl -sI https://monitor.sam.it.kr | head -3
|
||||
|
||||
# 리소스 확인
|
||||
free -h && df -h
|
||||
|
||||
# 모니터링 연결
|
||||
curl -s http://localhost:9090/api/v1/targets | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for t in data['data']['activeTargets']:
|
||||
print(f\"{t['labels'].get('job','?'):15} {t['health']:6} {t['scrapeUrl']}\")
|
||||
"
|
||||
|
||||
# MySQL 상태
|
||||
mysql -e "SHOW GLOBAL STATUS LIKE 'Uptime';"
|
||||
|
||||
# MySQL 리플리케이션 상태
|
||||
mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|SQL_Running|Behind|Error'
|
||||
# → IO_Running: Yes, SQL_Running: Yes, Seconds_Behind: 0
|
||||
```
|
||||
|
||||
**자동 시작 확인:**
|
||||
|
||||
```bash
|
||||
for svc in nginx jenkins gitea mysql prometheus grafana-server node_exporter fail2ban; do
|
||||
echo -n "$svc: "
|
||||
systemctl is-enabled $svc 2>/dev/null || echo "NOT FOUND"
|
||||
done
|
||||
# 비활성 서비스: sudo systemctl enable 서비스명
|
||||
```
|
||||
1274
dev/deploys/ops-manual/11-server-setup.md
Normal file
1274
dev/deploys/ops-manual/11-server-setup.md
Normal file
File diff suppressed because it is too large
Load Diff
42
dev/deploys/ops-manual/README.md
Normal file
42
dev/deploys/ops-manual/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# SAM 인프라 운영 매뉴얼
|
||||
|
||||
> 작성일: 2026-02-24
|
||||
> 대상: SAM 프로젝트 운영팀
|
||||
|
||||
---
|
||||
|
||||
## 서버 현황
|
||||
|
||||
| 서버 | IP | SSH 별칭 | 용도 |
|
||||
|------|-----|---------|------|
|
||||
| **운영** | 211.117.60.189 | `sam-prod` | 웹 서비스 (7개 도메인) |
|
||||
| **CI/CD** | 110.10.147.46 | `sam-cicd` | Jenkins, Gitea, 모니터링 |
|
||||
| **개발** | 114.203.209.83 | `sam-dev` | 개발 환경 |
|
||||
|
||||
## 문서 목차
|
||||
|
||||
| # | 문서 | 내용 |
|
||||
|---|------|------|
|
||||
| 1 | [서버 인프라 개요](./01-server-overview.md) | 서버 사양, 도메인, 서비스 현황, 디렉토리 구조, 설정 경로 |
|
||||
| 2 | [일상 운영](./02-daily-operations.md) | 상태 확인, 리소스 모니터링, 로그 확인, SSL 인증서 |
|
||||
| 3 | [운영서버 서비스 관리](./03-service-prod.md) | Nginx, PHP-FPM, MySQL, Redis, PM2, Supervisor 등 |
|
||||
| 4 | [CI/CD 서비스 관리](./04-service-cicd.md) | Jenkins, Gitea, Prometheus, Grafana 등 |
|
||||
| 5 | [배포 가이드](./05-deployment.md) | Jenkins 파이프라인, 수동 배포, 롤백, Gitea 연동 |
|
||||
| 6 | [데이터베이스 관리](./06-database.md) | MySQL, Redis 접속/백업/복구/성능 |
|
||||
| 7 | [모니터링](./07-monitoring.md) | Prometheus, Grafana, PromQL, 성능 분석 |
|
||||
| 8 | [장애 대응](./08-troubleshooting.md) | 운영/CI/CD 장애 시나리오별 진단 및 조치 |
|
||||
| 9 | [보안 관리](./09-security.md) | SSH, UFW, SSL, fail2ban, 접근 제어 |
|
||||
| 10 | [백업/복구/재부팅](./10-backup-recovery.md) | DB/파일 백업, 서버 복구, 재부팅 절차 |
|
||||
| 11 | [서버 설치 가이드](./11-server-setup.md) | 운영/CI/CD 서버 설치 절차, 설정 템플릿, 보안 체크리스트 |
|
||||
|
||||
## 빠른 접속
|
||||
|
||||
```bash
|
||||
ssh sam-prod # 운영서버
|
||||
ssh sam-cicd # CI/CD 서버
|
||||
ssh sam-dev # 개발서버
|
||||
```
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- 서버 설치 절차는 [11. 서버 설치 가이드](./11-server-setup.md) 참조
|
||||
Reference in New Issue
Block a user