From 9610feafc0ab27179cdc1de410b3391cb809ac26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 24 Feb 2026 09:58:06 +0900 Subject: [PATCH] =?UTF-8?q?docs(DOC):=20=EC=9A=B4=EC=98=81=20=EB=A7=A4?= =?UTF-8?q?=EB=89=B4=EC=96=BC=20=EA=B5=AC=EC=A1=B0=ED=99=94=20=E2=80=94=20?= =?UTF-8?q?ops-manual/=2011=EA=B0=9C=20=EC=A3=BC=EC=A0=9C=EB=B3=84=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영/CI/CD 서버 셋업 가이드를 ops-manual/에 통합 - 11-server-setup.md: 운영+CI/CD 서버 설치 절차 통합 - 05-deployment.md: Jenkinsfile 코드, Git 동기화 전략, 배포 흐름도 추가 - 원본 파일 삭제: production-server-setup.md, cicd-server-setup.md Co-Authored-By: Claude Opus 4.6 --- deploys/cicd-server-setup.md | 1208 --------------------- deploys/ops-manual/01-server-overview.md | 276 +++++ deploys/ops-manual/02-daily-operations.md | 235 ++++ deploys/ops-manual/03-service-prod.md | 274 +++++ deploys/ops-manual/04-service-cicd.md | 318 ++++++ deploys/ops-manual/05-deployment.md | 765 +++++++++++++ deploys/ops-manual/06-database.md | 203 ++++ deploys/ops-manual/07-monitoring.md | 253 +++++ deploys/ops-manual/08-troubleshooting.md | 522 +++++++++ deploys/ops-manual/09-security.md | 206 ++++ deploys/ops-manual/10-backup-recovery.md | 355 ++++++ deploys/ops-manual/11-server-setup.md | 1018 +++++++++++++++++ deploys/ops-manual/README.md | 42 + deploys/production-server-setup.md | 1110 ------------------- 14 files changed, 4467 insertions(+), 2318 deletions(-) delete mode 100644 deploys/cicd-server-setup.md create mode 100644 deploys/ops-manual/01-server-overview.md create mode 100644 deploys/ops-manual/02-daily-operations.md create mode 100644 deploys/ops-manual/03-service-prod.md create mode 100644 deploys/ops-manual/04-service-cicd.md create mode 100644 deploys/ops-manual/05-deployment.md create mode 100644 deploys/ops-manual/06-database.md create mode 100644 deploys/ops-manual/07-monitoring.md create mode 100644 deploys/ops-manual/08-troubleshooting.md create mode 100644 deploys/ops-manual/09-security.md create mode 100644 deploys/ops-manual/10-backup-recovery.md create mode 100644 deploys/ops-manual/11-server-setup.md create mode 100644 deploys/ops-manual/README.md delete mode 100644 deploys/production-server-setup.md diff --git a/deploys/cicd-server-setup.md b/deploys/cicd-server-setup.md deleted file mode 100644 index af1cc8e..0000000 --- a/deploys/cicd-server-setup.md +++ /dev/null @@ -1,1208 +0,0 @@ -# CI/CD 서버 셋팅 가이드 - -> 작성일: 2026-02-23 | 최종 수정: 2026-02-24 -> 상태: ✅ 설치 및 배포 테스트 완료 (react develop 파이프라인 검증 완료) - ---- - -## 1. 서버 구성 개요 - -### 인프라 구조 - -``` -┌──────────────────────────────────────────────────────────────┐ -│ CI/CD서버 (2 vCPU / 8GB) │ -│ Ubuntu 24.04 / IDC 클라우드 │ -│ IP: 110.10.147.46 │ -│ │ -│ ┌──────────┐ ┌───────────┐ ┌───────────────────────────┐ │ -│ │ Nginx │ │ Certbot │ │ UFW (22,80,443) │ │ -│ │ (Proxy) │ │ (SSL) │ │ │ │ -│ └────┬─────┘ └───────────┘ └───────────────────────────┘ │ -│ │ │ -│ ┌────┴───────────────────────────────────────────────────┐ │ -│ │ Virtual Hosts │ │ -│ │ │ │ -│ │ 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) │ │ -│ └──────────────┘ └──────────────┘ │ -└───────────────────────────────────────────────────────────────┘ -``` - -### 도메인 매핑 - -| 도메인 | 서비스 | 포트 | 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 | - -### 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/stage/main) - ▼ -개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자의 origin - │ - ├─ develop push 시 - │ ├─ api/mng/sales: 기존 post-update hook (개발서버 pull) ← 현행 유지 - │ └─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 개발서버 배포 - │ - ├─ stage push 시 - │ ├─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 운영서버 Stage 배포 - │ └─ api: hook → CI/CD Gitea push → Jenkins → 운영서버 Stage pull - │ - └─ main push 시 (react/mng/api) - └─ ❌ CI/CD Gitea에 자동 push 안함 - → 배포관리자가 수동으로 CI/CD Gitea에 push - → Jenkins 자동 배포 - -별도 처리: - sales/www(landing): Push Mirror 또는 hook → CI/CD Gitea → Jenkins → 운영서버 pull -``` - -### 브랜치별 배포 정책 상세 - -| 브랜치 | 저장소 | CI/CD Gitea 동기화 | Jenkins 배포 | 배포 대상 | -|--------|--------|-------------------|-------------|----------| -| **stage** | react | 자동 (hook) | 빌드 + rsync | 운영서버 Stage | -| **stage** | api | 자동 (hook) | SSH pull | 운영서버 Stage | -| **main** | react | 수동 (배포관리자) | 빌드 + rsync | 운영서버 Production | -| **main** | mng | 수동 (배포관리자) | SSH deploy | 운영서버 Production | -| **main** | api | 수동 (배포관리자) | SSH deploy | 운영서버 Production | -| **main** | sales | 자동 (hook/mirror) | SSH pull | 운영서버 Production | -| **main** | www | 자동 (hook/mirror) | SSH pull | 운영서버 Production | -| **develop** | react | 자동 (hook) | 빌드 → 개발서버 배포 | 개발서버 | -| **develop** | api | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | -| **develop** | mng | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | -| **develop** | sales | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | - -### 배포관리자 운영 배포 워크플로우 - -```bash -# 배포관리자 로컬에서 (react/mng/api 저장소) -# CI/CD Gitea를 별도 remote로 등록 (1회) -git remote add production https://git.sam.it.kr/SamProject/sam-api.git - -# 운영 배포 시: main 브랜치를 CI/CD Gitea에 push -git push production main -# → CI/CD Gitea webhook → Jenkins → 운영서버 자동 배포 -``` - -현재 Git remote 현황 (개발서버): -``` -sam-api: http://114.203.209.83:3000/SamProject/sam-api.git -sam-manage: http://114.203.209.83:3000/SamProject/sam-manage.git -sam-react-prod: http://114.203.209.83:3000/SamProject/sam-react-prod.git -sam-sales: http://114.203.209.83:3000/SamProject/sam-sales.git -sam-docs: http://114.203.209.83:3000/SamProject/sam-docs.git -sam-design: http://114.203.209.83:3000/SamProject/sam-design.git -sam-planning: http://114.203.209.83:3000/SamProject/sam-planning.git -``` - ---- - -## 2. 메모리 배분 계획 (8GB) - -| 서비스 | 할당 | 설정 | 비고 | -|--------|------|------|------| -| Jenkins | ~2.0GB | -Xmx2048m | Java 기반, 빌드 시 메모리 소모 큼 | -| MySQL 8.4 | ~1.5GB | innodb_buffer_pool_size=1536M | Gitea DB + 운영 백업 | -| Gitea | ~0.5GB | - | Go 기반, 가벼움 | -| Prometheus | ~0.5GB | --storage.tsdb.retention.time=30d | 메트릭 저장 30일 | -| Grafana | ~0.3GB | - | 대시보드 시각화 | -| Nginx | ~0.1GB | - | 리버스 프록시 | -| node_exporter | ~0.01GB | - | 자체 모니터링 | -| OS + 여유 | ~3.1GB | 스왑 4GB | 안전 마진 | -| **합계** | **~8GB** | | **스왑 4GB 백업** | - ---- - -## 3. 설치 순서 - -### ① OS 기본 셋팅 ✅ - -```bash -# 시스템 업데이트 -sudo apt update && sudo apt upgrade -y - -# 기본 패키지 -sudo apt install -y curl wget git unzip vim htop net-tools software-properties-common - -# 타임존 -sudo timedatectl set-timezone Asia/Seoul - -# 스왑 4GB 설정 -sudo fallocate -l 4G /swapfile -sudo chmod 600 /swapfile -sudo mkswap /swapfile -sudo swapon /swapfile -echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab - -# 스왑 최적화 -echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf -sudo sysctl -p - -# UFW 방화벽 -sudo ufw default deny incoming -sudo ufw default allow outgoing -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS -sudo ufw enable - -# webservice 사용자 그룹 (배포 스크립트용) -sudo groupadd -f webservice -sudo usermod -aG webservice hskwon -``` - -### ② MySQL 8.4 ✅ - -> **설치 완료**: MySQL 8.4.8 (GPG 키 만료 이슈로 Ubuntu keyserver 경유 설치) -> **주의**: `validate_password.policy` 설정은 플러그인 미로딩 시 MySQL 시작 실패 — 사용하지 않음 - -```bash -# 1. mysql-apt-config deb로 repo 등록 -sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb -sudo DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.33-1_all.deb - -# 2. GPG 키 만료 시 — Ubuntu keyserver에서 갱신 -sudo gpg --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C -sudo gpg --export B7B3B788A8D3785C | sudo tee /usr/share/keyrings/mysql-apt-config.gpg > /dev/null - -# 3. 설치 -sudo apt update -sudo apt install -y mysql-server -``` - -**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): - -```ini -[mysqld] -innodb_buffer_pool_size = 1536M -innodb_redo_log_capacity = 536870912 -innodb_flush_log_at_trx_commit = 2 -max_connections = 50 -character-set-server = utf8mb4 -collation-server = utf8mb4_unicode_ci -slow_query_log = 1 -slow_query_log_file = /var/log/mysql/slow.log -long_query_time = 2 - -# Replication (Slave) — 운영서버 Master 설정 완료 후 활성화 -# server-id = 2 -# relay-log = /var/log/mysql/mysql-relay-bin -# read-only = 1 ← Gitea 로컬 DB 쓰기를 차단하므로 반드시 주석 유지 -``` - -```bash -sudo systemctl restart mysql -``` - -**DB 및 사용자:** - -```sql --- Gitea DB -CREATE DATABASE gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'gitea'@'localhost' IDENTIFIED BY ''; -GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'localhost'; - --- 관리자 (hskwon) - auth_socket 인증 -CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; -GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; - -FLUSH PRIVILEGES; -``` - -> **참고**: root는 auth_socket 인증 (비밀번호 없이 `sudo mysql`로 접근) - -### ②-slave MySQL Replication 설정 (운영 DB 백업) - -> 운영서버 MySQL Master → CI/CD 서버 MySQL Slave - -**운영서버(Master)에서 실행:** - -```sql -CREATE USER 'repl_user'@'110.10.147.46' IDENTIFIED BY '<복제_비밀번호>'; -GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'110.10.147.46'; -FLUSH PRIVILEGES; -SHOW MASTER STATUS; --- → File: mysql-bin.XXXXXX, Position: XXXX 기록 -``` - -**운영서버 my.cnf 추가:** -```ini -[mysqld] -server-id = 1 -log-bin = /var/log/mysql/mysql-bin -binlog-do-db = sam_production -binlog-do-db = chandj -``` - -**CI/CD서버(Slave)에서 실행:** - -```sql -CHANGE REPLICATION SOURCE TO - SOURCE_HOST='211.117.60.189', - SOURCE_USER='repl_user', - SOURCE_PASSWORD='<복제_비밀번호>', - SOURCE_LOG_FILE='mysql-bin.XXXXXX', - SOURCE_LOG_POS=XXXX; - -START REPLICA; -SHOW REPLICA STATUS\G --- Slave_IO_Running: Yes, Slave_SQL_Running: Yes 확인 -``` - -### ③ Redis → 운영서버에 설치 - -> **Redis는 운영서버(211.117.60.189)에 설치.** -> 상세 설정: [production-server-setup.md](./production-server-setup.md) Redis 섹션 참조 - -### ④ Java (Jenkins 의존) ✅ - -> **설치 완료**: OpenJDK 17.0.18 - -```bash -sudo apt install -y openjdk-17-jre-headless -java -version -``` - -### ⑤ Gitea ✅ - -> **설치 완료**: Gitea 1.22.6 -> - 관리자: hskwon / kent@codebridge-x.com -> - Organization: SamProject -> - 저장소: sam-api, sam-manage, sam-react-prod, sam-sales, sam-landing -> - 회원가입 비활성화, API 토큰 생성 완료 - -```bash -GITEA_VERSION="1.22.6" -wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64 -sudo mv /tmp/gitea /usr/local/bin/gitea -sudo chmod +x /usr/local/bin/gitea - -sudo adduser --system --group --disabled-password --shell /bin/bash --home /home/git git - -sudo mkdir -p /var/lib/gitea/{custom,data,log} -sudo mkdir -p /etc/gitea -sudo chown -R git:git /var/lib/gitea -sudo chown git:git /etc/gitea -sudo chmod 750 /etc/gitea -``` - -**systemd 서비스 등록:** - -```bash -sudo tee /etc/systemd/system/gitea.service > /dev/null << 'EOF' -[Unit] -Description=Gitea (Git with a cup of tea) -After=syslog.target network.target mysql.service - -[Service] -Type=simple -User=git -Group=git -WorkingDirectory=/var/lib/gitea/ -ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini -Restart=always -Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea -RestartSec=10 - -[Install] -WantedBy=multi-user.target -EOF - -sudo systemctl daemon-reload -sudo systemctl enable gitea -``` - -**Gitea 설정** (`/etc/gitea/app.ini`): - -```ini -[server] -DOMAIN = git.sam.it.kr -HTTP_PORT = 3000 -ROOT_URL = https://git.sam.it.kr/ -DISABLE_SSH = false -SSH_PORT = 22 -LFS_START_SERVER = true - -[database] -DB_TYPE = mysql -HOST = 127.0.0.1:3306 -NAME = gitea -USER = gitea -PASSWD = -CHARSET = utf8mb4 - -[repository] -ROOT = /var/lib/gitea/data/repositories - -[log] -ROOT_PATH = /var/lib/gitea/log -MODE = file -LEVEL = info - -[service] -DISABLE_REGISTRATION = true -REQUIRE_SIGNIN_VIEW = true -``` - -```bash -sudo systemctl start gitea -# 확인: curl http://localhost:3000 -``` - -**CI/CD Gitea 초기 설정:** - -``` -1. https://git.sam.it.kr 웹 설치 마법사 완료 -2. 관리자 계정 생성 -3. Organization "SamProject" 생성 -4. 빈 저장소 생성: - - sam-api, sam-manage, sam-react-prod - - sam-sales, sam-landing (www) -5. Jenkins Webhook용 API 토큰 생성 -``` - -### ⑤-hook 개발서버 post-receive hook 설정 (선택적 브랜치 동기화) ✅ - -> **설치 완료**: 개발서버 Gitea bare repository에 post-receive hook 추가 -> stage/develop(react만) push 시 CI/CD Gitea에 해당 브랜치만 push. -> 기존 post-update hook (개발서버 pull)은 **그대로 유지**. - -**개발서버 Gitea bare repo 경로:** - -``` -/data/GIT/samproject/.git/hooks/post-receive.d/push-to-cicd -``` - -**토큰 보안 (환경변수 파일):** - -```bash -# /data/GIT/.cicd-env (chmod 600, owner: git) -CICD_GITEA_TOKEN=<토큰> -CICD_GITEA_USER=hskwon -CICD_GITEA_HOST=git.sam.it.kr -``` - -**hook 스크립트 구조 (공통 패턴):** - -```bash -#!/bin/bash -source /data/GIT/.cicd-env -LOGFILE=/home/webservice/logs/cicd_push_.log -CICD_REMOTE="https://${CICD_GITEA_USER}:${CICD_GITEA_TOKEN}@${CICD_GITEA_HOST}/SamProject/.git" - -mkdir -p /home/webservice/logs - -while read oldrev newrev refname; do - BRANCH=$(echo "$refname" | sed 's|refs/heads/||') - if [ "$BRANCH" = "" ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S'): Pushing ${BRANCH} to CI/CD Gitea" >> $LOGFILE - git push $CICD_REMOTE ${BRANCH}:${BRANCH} >> $LOGFILE 2>&1 - echo "$(date '+%Y-%m-%d %H:%M:%S'): Done (exit: $?)" >> $LOGFILE - fi -done -``` - -> **참고**: Gitea는 `hooks/post-receive.d/` 디렉토리의 스크립트를 자동 실행. -> 기존 post-update hook (pull 스크립트)과 별도로 동작. - -**동기화 요약:** - -| 저장소 | hook 대상 브랜치 | 동작 | -|--------|-----------------|------| -| sam-react-prod | stage, develop | CI/CD Gitea에 push | -| sam-api | stage | CI/CD Gitea에 push | -| sam-sales | main | CI/CD Gitea에 push | -| sam-landing | main | CI/CD Gitea에 push | -| sam-manage | ❌ 없음 | main만 사용, 배포관리자 수동 push | - -### ⑥ Jenkins ✅ - -> **설치 완료**: Jenkins 2.541.2 -> - 관리자: hskwon -> - 플러그인: Gitea, SSH Agent, Pipeline Stage View, Workflow Aggregator, Blue Ocean, NodeJS -> - Gitea 서버 연동 완료 (https://git.sam.it.kr) -> - SSH Credential 등록: `deploy-ssh-key` (운영/개발 서버 모두 접속 확인) -> - Gitea API Token Credential 등록: `gitea-api-token` -> - Node.js 22.22.0 설치 완료 - -```bash -# Jenkins GPG 키 + APT Repository -# 주의: 공식 2023 키가 유효하지 않을 수 있음 → Ubuntu keyserver에서 획득 -sudo gpg --keyserver keyserver.ubuntu.com --recv-keys 7198F4B714ABFC68 -sudo gpg --export 7198F4B714ABFC68 | sudo tee /usr/share/keyrings/jenkins-keyring.gpg > /dev/null -echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.gpg]" \ - https://pkg.jenkins.io/debian-stable binary/ | sudo tee \ - /etc/apt/sources.list.d/jenkins.list > /dev/null - -sudo apt update -sudo apt install -y jenkins - -# Jenkins JVM 메모리 제한 -sudo mkdir -p /etc/systemd/system/jenkins.service.d -sudo tee /etc/systemd/system/jenkins.service.d/override.conf > /dev/null << 'EOF' -[Service] -Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true" -EOF - -sudo systemctl daemon-reload -sudo systemctl enable jenkins -sudo systemctl start jenkins - -# 초기 관리자 비밀번호 -sudo cat /var/lib/jenkins/secrets/initialAdminPassword -``` - -**Jenkins에 필요한 도구 설치:** - -```bash -# Node.js 22 (react 빌드용) -curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - -sudo apt install -y nodejs - -# PHP 8.4 + Composer (Laravel 테스트/린트용 — 선택사항) -sudo add-apt-repository ppa:ondrej/php -y -sudo apt update -sudo apt install -y php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-mysql php8.4-bcmath -curl -sS https://getcomposer.org/installer | php -sudo mv composer.phar /usr/local/bin/composer -``` - -**Jenkins 초기 설정 (웹 UI):** - -1. `https://ci.sam.it.kr` 접속 -2. 초기 비밀번호 입력 -3. 추천 플러그인 설치 + 추가 플러그인: - - **Gitea Plugin** (Gitea webhook 연동) - - **SSH Agent Plugin** (운영/개발서버 SSH 배포) - - **Pipeline** (Jenkinsfile 지원) - - **Blue Ocean** (모던 UI, 선택) - -4. Jenkins SSH 키 설정: -```bash -# Jenkins 사용자로 SSH 키 생성 -sudo -u jenkins ssh-keygen -t ed25519 -C "jenkins@sam-cicd" -f /var/lib/jenkins/.ssh/id_ed25519 -N "" - -# 운영서버에 공개키 등록 -ssh sam-prod "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" - -# 개발서버에도 공개키 등록 (develop react 빌드 배포용) -ssh sam-dev "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" - -# SSH 호스트 키 등록 (known_hosts) -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 -``` - -5. Jenkins Credentials 설정 (CLI로 완료): - - `deploy-ssh-key`: SSH 키 (hskwon@운영/개발 서버 공용) - - `gitea-api-token`: Gitea API 토큰 - -### ⑦ Nginx + Certbot ✅ - -> **설치 완료**: Nginx + Let's Encrypt SSL (git/ci/monitor.sam.it.kr) - -```bash -sudo apt install -y nginx certbot python3-certbot-nginx -``` - -**Gitea 리버스 프록시** (`/etc/nginx/sites-available/git.sam.it.kr`): - -```nginx -server { - listen 80; - server_name git.sam.it.kr; - client_max_body_size 100M; - - location / { - proxy_pass http://127.0.0.1:3000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -**Jenkins 리버스 프록시** (`/etc/nginx/sites-available/ci.sam.it.kr`): - -```nginx -server { - listen 80; - server_name ci.sam.it.kr; - - location / { - proxy_pass http://127.0.0.1:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 90; - proxy_buffering off; - } -} -``` - -**Grafana 리버스 프록시** (`/etc/nginx/sites-available/monitor.sam.it.kr`): - -```nginx -server { - listen 80; - server_name monitor.sam.it.kr; - - location / { - proxy_pass http://127.0.0.1:3100; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } -} -``` - -```bash -# 사이트 활성화 + SSL -sudo ln -s /etc/nginx/sites-available/git.sam.it.kr /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/ci.sam.it.kr /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/monitor.sam.it.kr /etc/nginx/sites-enabled/ -sudo nginx -t && sudo systemctl reload nginx - -sudo certbot --nginx -d git.sam.it.kr -sudo certbot --nginx -d ci.sam.it.kr -sudo certbot --nginx -d monitor.sam.it.kr -sudo certbot renew --dry-run -``` - -### ⑧ Prometheus + node_exporter ✅ - -> **설치 완료**: Prometheus 2.51.0, node_exporter 1.8.2 - -```bash -# Prometheus -PROM_VERSION="2.51.0" -cd /tmp -wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz -tar xzf prometheus-${PROM_VERSION}.linux-amd64.tar.gz -sudo mv prometheus-${PROM_VERSION}.linux-amd64/prometheus /usr/local/bin/ -sudo mv prometheus-${PROM_VERSION}.linux-amd64/promtool /usr/local/bin/ -sudo mkdir -p /etc/prometheus /var/lib/prometheus -sudo mv prometheus-${PROM_VERSION}.linux-amd64/consoles /etc/prometheus/ -sudo mv prometheus-${PROM_VERSION}.linux-amd64/console_libraries /etc/prometheus/ -rm -rf prometheus-${PROM_VERSION}* -sudo useradd --no-create-home --shell /bin/false prometheus -sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus -``` - -**Prometheus 설정** (`/etc/prometheus/prometheus.yml`): - -```yaml -global: - scrape_interval: 15s - evaluation_interval: 15s - -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' -``` - -```bash -# Prometheus systemd -sudo tee /etc/systemd/system/prometheus.service > /dev/null << 'EOF' -[Unit] -Description=Prometheus -After=network-online.target - -[Service] -User=prometheus -Group=prometheus -Type=simple -ExecStart=/usr/local/bin/prometheus \ - --config.file=/etc/prometheus/prometheus.yml \ - --storage.tsdb.path=/var/lib/prometheus/ \ - --storage.tsdb.retention.time=30d \ - --web.listen-address=127.0.0.1:9090 -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - -# node_exporter -cd /tmp -wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz -tar xzf node_exporter-1.8.2.linux-amd64.tar.gz -sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/ -rm -rf node_exporter-1.8.2* - -sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF' -[Unit] -Description=Node Exporter -After=network.target - -[Service] -User=nobody -ExecStart=/usr/local/bin/node_exporter -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - -sudo systemctl daemon-reload -sudo systemctl enable prometheus node_exporter -sudo systemctl start prometheus node_exporter -``` - -### ⑨ Grafana ✅ - -> **설치 완료**: Grafana (포트 3100) - -```bash -sudo mkdir -p /etc/apt/keyrings/ -wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null -echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list -sudo apt update -sudo apt install -y grafana -``` - -**Grafana 설정** (`/etc/grafana/grafana.ini`): - -```ini -[server] -http_port = 3100 -domain = monitor.sam.it.kr -root_url = https://monitor.sam.it.kr/ - -[security] -admin_password = - -[users] -allow_sign_up = false -``` - -```bash -sudo systemctl enable grafana-server -sudo systemctl start grafana-server -``` - -**Grafana 초기 설정 (웹 UI):** - -1. Data Source: Prometheus → `http://localhost:9090` -2. 대시보드 임포트: **Node Exporter Full** (ID: 1860) - ---- - -## 4. 배포 파이프라인 설계 - -### Webhook 설정 (CI/CD Gitea → Jenkins) - -각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI): - -``` -Repository Settings → Webhooks → Add Webhook (Gitea) -- URL: https://ci.sam.it.kr/gitea-webhook/post -- Content Type: application/json -- Secret: -- Events: Push events -``` - -### 파이프라인: Laravel API (api/) - -**Jenkinsfile** (`api/Jenkinsfile`): - -```groovy -pipeline { - agent any - - environment { - DEPLOY_USER = 'hskwon' - APP_NAME = 'api' - RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') - } - - stages { - stage('Checkout') { - steps { checkout scm } - } - - // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── - stage('Deploy Production') { - when { branch 'main' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 ' - cd /home/webservice/api/releases && - git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && - ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage && - ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env && - cd /home/webservice/api/releases/${RELEASE_ID} && - composer install --no-dev --optimize-autoloader --no-interaction && - php artisan config:cache && - php artisan route:cache && - php artisan view:cache && - php artisan migrate --force && - ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current && - sudo systemctl reload php8.4-fpm && - sudo supervisorctl restart sam-queue-worker:* && - cd /home/webservice/api/releases && - ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true - ' - """ - } - } - } - - // ── stage → 운영서버 Stage ── - stage('Deploy Stage') { - when { branch 'stage' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 ' - cd /home/webservice/api-stage/releases && - git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && - ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/${RELEASE_ID}/storage && - ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/${RELEASE_ID}/.env && - cd /home/webservice/api-stage/releases/${RELEASE_ID} && - composer install --no-dev --optimize-autoloader --no-interaction && - php artisan config:cache && - php artisan route:cache && - php artisan view:cache && - php artisan migrate --force && - ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current && - sudo systemctl reload php8.4-fpm && - cd /home/webservice/api-stage/releases && - ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true - ' - """ - } - } - } - - // develop → Jenkins 관여 안함 (기존 post-update hook 유지) - } - - post { - success { echo "✅ api 배포 완료 (${env.BRANCH_NAME})" } - failure { - echo "❌ api 배포 실패 (${env.BRANCH_NAME})" - // 운영/Stage 실패 시 이전 릴리즈로 롤백 - script { - if (env.BRANCH_NAME in ['main', 'stage']) { - def baseDir = env.BRANCH_NAME == 'main' - ? '/home/webservice/api' - : '/home/webservice/api-stage' - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 ' - PREV=\$(ls -1dt ${baseDir}/releases/*/ | sed -n "2p" | xargs basename) && - [ -n "\$PREV" ] && ln -sfn ${baseDir}/releases/\$PREV ${baseDir}/current && - sudo systemctl reload php8.4-fpm - ' - """ - } - } - } - } - } -} -``` - -### 파이프라인: Laravel Admin (mng/) - -> API와 동일 구조. 차이점: -> - main만 (stage/develop 없음 — develop은 기존 post-update hook) -> - Queue Worker 재시작 불필요 -> - npm run build 추가 (Blade + Vite) - -### 파이프라인: Next.js React (react/) - -> **검증 완료**: Jenkins Build #3 (develop) — 빌드 + 배포 성공 (241초) -> - rsync source trailing slash 주의: `.next/` (X) → `.next` (O) -> - 개발서버 PM2: `sam-react` (fork), 운영서버 PM2: `sam-front` (cluster) -> - `npm install --prefer-offline` 사용 (package-lock.json이 .gitignore에 포함) - -**Jenkinsfile** (`react/Jenkinsfile`) — 실제 배포 검증 완료: - -```groovy -pipeline { - agent any - - environment { - DEPLOY_USER = 'hskwon' - RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') - } - - stages { - stage('Checkout') { - steps { checkout scm } - } - - stage('Prepare Env') { - steps { - script { - def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" - sh "cp ${envFile} .env.local" - } - } - } - - stage('Install') { - steps { - sh 'npm install --prefer-offline' - } - } - - stage('Build') { - steps { - sh 'npm run build' - } - } - - // ── develop → 개발서버 배포 ── - stage('Deploy Development') { - when { branch 'develop' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - rsync -az --delete \ - --exclude='.git' \ - --exclude='.env*' \ - --exclude='ecosystem.config.*' \ - .next package.json next.config.ts public node_modules \ - ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ - - scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local - - ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' - """ - } - } - } - - // ── stage → 운영서버 Stage 배포 ── - stage('Deploy Stage') { - when { branch 'stage' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' - - rsync -az --delete \ - .next package.json next.config.ts public node_modules \ - ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ - - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local - - ssh ${DEPLOY_USER}@211.117.60.189 ' - ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && - cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && - cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true - ' - """ - } - } - } - - // ── main → 운영서버 Production 배포 ── - stage('Deploy Production') { - when { branch 'main' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' - - rsync -az --delete \ - .next package.json next.config.ts public node_modules \ - ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ - - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local - - ssh ${DEPLOY_USER}@211.117.60.189 ' - ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && - cd /home/webservice && pm2 reload sam-front && - cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true - ' - """ - } - } - } - } - - post { - success { echo '✅ react 배포 완료 (' + env.BRANCH_NAME + ')' } - failure { echo '❌ react 배포 실패 (' + env.BRANCH_NAME + ')' } - } -} -``` - -**환경별 .env 파일** (`/var/lib/jenkins/env-files/react/`): - -| 파일 | API URL | Frontend URL | -|------|---------|-------------| -| `.env.develop` | `https://api.codebridge-x.com` | `https://dev.codebridge-x.com` | -| `.env.stage` | `https://stage-api.sam.it.kr` | `https://stage.sam.it.kr` | -| `.env.main` | `https://api.sam.it.kr` | `https://sam.it.kr` | - -> **주의사항 (rsync)**: -> - 소스 경로에 trailing slash 사용 금지: `.next/` → 내용만 복사됨, `.next` → 디렉토리째 복사 -> - `--delete`와 함께 사용 시 `--exclude`로 `.git`, `.env*` 보호 필수 (개발서버) -> - Release 기반 배포 (stage/main)는 새 디렉토리이므로 exclude 불필요 - -### 파이프라인: Sales (레거시 PHP) - -```groovy -pipeline { - agent any - environment { DEPLOY_USER = 'hskwon' } - - stages { - // main → 운영서버 (hook 자동) - stage('Deploy Production') { - when { branch 'main' } - steps { - sshagent(credentials: ['deploy-ssh-key']) { - sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/sales && git pull origin main'" - } - } - } - // develop → 개발서버는 기존 post-update hook 유지 - } -} -``` - -### 파이프라인: Landing (www) - -```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'" - } - } - } - } -} -``` - ---- - -## 5. 배포 흐름도 - -``` -개발자 로컬 - │ git push origin (develop / stage / 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) │ │ -│ └───────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ stage push ──────────────────────────────────────────┐ │ -│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ -│ │ → rsync → 운영서버 Stage + PM2 reload │ │ -│ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │ -│ │ → 운영서버 Stage Release + 심링크 │ │ -│ └───────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ main push (sales/www만 자동) ────────────────────────┐ │ -│ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │ -│ │ → 운영서버 pull │ │ -│ │ www → hook: CI/CD Gitea push ──→ Jenkins │ │ -│ │ → 운영서버 pull │ │ -│ │ react/mng/api → ❌ 자동 push 안함 │ │ -│ └───────────────────────────────────────────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ - -┌─ 운영 배포 (main - react/mng/api) ──────────────────────────┐ -│ │ -│ 배포관리자 로컬 │ -│ │ git push production main (CI/CD Gitea remote) │ -│ ▼ │ -│ CI/CD Gitea (git.sam.it.kr) │ -│ │ Webhook │ -│ ▼ │ -│ Jenkins → 운영서버 배포 │ -│ react: CI/CD 빌드 → rsync → PM2 reload │ -│ api: Release + 심링크 → PHP-FPM reload │ -│ mng: Release + 심링크 → PHP-FPM reload │ -│ │ -└───────────────────────────────────────────────────────────────┘ -``` - -### 환경별 배포 비교 - -| 항목 | 운영 (main) | Stage (stage) | 개발 (develop) | -|------|------------|---------------|----------------| -| **트리거** | 배포관리자 수동 push | 자동 (hook) | react만 자동 (hook), 나머지 기존 hook | -| **react 전략** | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | -| **api 전략** | Release + 심링크 | Release + 심링크 | 기존 post-update (pull) | -| **mng 전략** | Release + 심링크 | - | 기존 post-update (pull + build) | -| **롤백** | 이전 릴리즈 심링크 | 이전 릴리즈 심링크 | git revert | -| **릴리즈 보관** | 최근 5개 | 최근 3개 | - | - ---- - -## 6. 백업 자동화 - -### DB 일일 백업 (CI/CD 서버 crontab) - -```bash -# /home/hskwon/scripts/backup-db.sh -#!/bin/bash -set -e - -BACKUP_DIR="/home/hskwon/backups/mysql" -DATE=$(date +%Y%m%d_%H%M%S) -RETENTION_DAYS=14 - -mkdir -p $BACKUP_DIR - -mysqldump --single-transaction --routines --triggers \ - sam_production > $BACKUP_DIR/sam_production_$DATE.sql -mysqldump --single-transaction --routines --triggers \ - chandj > $BACKUP_DIR/chandj_$DATE.sql - -gzip $BACKUP_DIR/sam_production_$DATE.sql -gzip $BACKUP_DIR/chandj_$DATE.sql -find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete - -echo "$(date): Backup completed" >> $BACKUP_DIR/backup.log -``` - -```bash -chmod +x /home/hskwon/scripts/backup-db.sh -# crontab (매일 새벽 3시) -(crontab -l 2>/dev/null; echo "0 3 * * * /home/hskwon/scripts/backup-db.sh") | crontab - -``` - ---- - -## 7. 최종 점검 - -```bash -# 서비스 상태 -sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter - -# 방화벽 / 메모리 / 디스크 / 포트 -sudo ufw status verbose -free -h -df -h -sudo ss -tlnp - -# 웹 접속 테스트 -curl -sI https://ci.sam.it.kr -curl -sI https://git.sam.it.kr -curl -sI https://monitor.sam.it.kr - -# SSL -sudo certbot certificates -``` - ---- - -## 8. 보안 체크리스트 - -- [x] SSH 키 인증만 허용 (PasswordAuthentication no) -- [x] root SSH 로그인 비활성화 (PermitRootLogin no) -- [x] UFW 방화벽 활성화 (22, 80, 443만 허용) -- [x] Jenkins 관리자 계정 변경 (hskwon) -- [ ] Jenkins CSRF 보호 활성화 -- [x] Gitea 회원가입 비활성화 (DISABLE_REGISTRATION = true) -- [x] Grafana 익명 접근 비활성화 (allow_sign_up = false) -- [x] Prometheus 외부 접근 차단 (127.0.0.1:9090 바인딩) -- [x] MySQL root 원격 접근 차단 (auth_socket 인증) -- [x] fail2ban 설치 (sshd jail 활성) -- [x] Certbot 자동 갱신 (certbot.timer 활성) -- [x] Jenkins SSH 키 ed25519 생성 + Credential 등록 -- [x] Webhook Secret 설정 (Gitea → Jenkins) -- [x] post-receive hook 토큰 보안 (/data/GIT/.cicd-env 파일 참조, 600 권한) - ---- - -## 9. 설치 순서 요약 - -| 순서 | 항목 | 예상 시간 | 의존성 | -|------|------|----------|--------| -| ① | OS 기본 셋팅 | 15분 | - | -| ② | MySQL 8.4 | 20분 | ① | -| ②-s | MySQL Replication | 30분 | ② + 운영서버 MySQL | -| ④ | Java 17 | 5분 | ① | -| ⑤ | Gitea 설치 + 초기 설정 | 30분 | ② | -| ⑤-h | 개발서버 post-receive hook 설정 | 30분 | ⑤ | -| ⑥ | Jenkins | 20분 | ④ | -| ⑦ | Nginx + SSL | 20분 | ⑤⑥ | -| ⑧ | Prometheus + node_exporter | 15분 | ① | -| ⑨ | Grafana | 15분 | ⑧ | -| ⑩ | Jenkins 파이프라인 + Webhook 설정 ✅ | 1시간 | ⑥⑦ | -| ⑪ | 백업 자동화 ✅ | 15분 | ②-s | -| ⑫ | 최종 점검 + 보안 ✅ | 30분 | 전체 | - -**총 예상 시간: 5~6시간** - ---- - -## 10. 결정 필요 사항 - -- [x] ~~Gitea 이전 여부~~ → 선택적 브랜치 동기화 (post-receive hook) -- [x] ~~브랜치 전략~~ → main(수동/운영), stage(자동/Stage), develop(react만 자동) -- [x] ~~도메인 확정~~ → git.sam.it.kr, ci.sam.it.kr, monitor.sam.it.kr (SSL 발급 완료) -- [ ] **Jenkins 테스트 실행 여부**: CI에서 phpunit/lint 실행 vs 배포만 -- [ ] **알림 채널**: Slack, 이메일, 카카오톡 등 -- [x] ~~개발서버 Gitea bare repo 경로 확인~~ → `/data/GIT/samproject/.git/` \ No newline at end of file diff --git a/deploys/ops-manual/01-server-overview.md b/deploys/ops-manual/01-server-overview.md new file mode 100644 index 0000000..545e608 --- /dev/null +++ b/deploys/ops-manual/01-server-overview.md @@ -0,0 +1,276 @@ +# 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 | +| admin.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 | 2 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 17.0.18 | - | 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 환경파일 | /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 | + +--- + +## 아키텍처 다이어그램 + +### 운영서버 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 운영서버 (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) │ │ +│ │ admin.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 | admin.codebridge-x.com | - | mng.codebridge-x.com | +| Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com | +| Landing | codebridge-x.com | - | - | \ No newline at end of file diff --git a/deploys/ops-manual/02-daily-operations.md b/deploys/ops-manual/02-daily-operations.md new file mode 100644 index 0000000..310ffc6 --- /dev/null +++ b/deploys/ops-manual/02-daily-operations.md @@ -0,0 +1,235 @@ +# 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 +``` + +--- + +## 시스템 리소스 모니터링 + +양쪽 서버 공통 명령어: + +```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/admin.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) 로그 +sudo tail -f /home/webservice/mng/shared/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" +``` \ No newline at end of file diff --git a/deploys/ops-manual/03-service-prod.md b/deploys/ops-manual/03-service-prod.md new file mode 100644 index 0000000..803b847 --- /dev/null +++ b/deploys/ops-manual/03-service-prod.md @@ -0,0 +1,274 @@ +# 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/admin.codebridge-x.com.access.log | Admin 접근 로그 | +| /var/log/nginx/admin.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 # 전체 삭제 (주의: 세션도 삭제됨) +``` + +**용도:** Laravel 캐시, 세션, 큐 (QUEUE_CONNECTION=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 # 활성화 +``` \ No newline at end of file diff --git a/deploys/ops-manual/04-service-cicd.md b/deploys/ops-manual/04-service-cicd.md new file mode 100644 index 0000000..cf11c59 --- /dev/null +++ b/deploys/ops-manual/04-service-cicd.md @@ -0,0 +1,318 @@ +# 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 파일) | + +**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 | Secret text | Gitea API 연동 | + +**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` + +### Workspace 정리 + +```bash +# 용량 확인 +sudo du -sh /var/lib/jenkins/workspace/* + +# 특정 workspace 삭제 +sudo rm -rf /var/lib/jenkins/workspace/ + +# 전체 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/.git/hooks/post-receive.d/push-to-cicd` + +**토큰 파일 (개발서버):** `/data/GIT/.cicd-env` (chmod 600, owner: git) + +| 저장소 | 동기화 브랜치 | +|--------|-------------| +| sam-react-prod | stage, develop | +| sam-api | stage | +| sam-sales | main | +| sam-manage | 없음 (배포관리자 수동 push) | + +**동기화 로그 확인:** + +```bash +ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_sam-react-prod.log" +ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_sam-api.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 리버스 프록시 | + +--- + +## node_exporter / Certbot / fail2ban / UFW + +운영서버와 동일한 명령어 체계. [운영서버 서비스 관리](./03-service-prod.md) 참조. + +**UFW 규칙 (CI/CD):** + +| 포트 | 프로토콜 | 용도 | +|------|---------|------| +| 22/tcp | ALLOW | SSH | +| 80/tcp | ALLOW | HTTP | +| 443/tcp | ALLOW | HTTPS | \ No newline at end of file diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md new file mode 100644 index 0000000..96c0b34 --- /dev/null +++ b/deploys/ops-manual/05-deployment.md @@ -0,0 +1,765 @@ +# 5. 배포 가이드 + +[목차로 돌아가기](./README.md) + +--- + +## 파이프라인 개요 + +### 전체 흐름 + +``` +개발자 push -> 개발서버 Gitea -> post-receive hook -> CI/CD Gitea push +-> Webhook -> Jenkins -> 빌드/배포 +``` + +### 파이프라인 구성 + +| 저장소 | 파이프라인 | 트리거 브랜치 | 배포 대상 | +|--------|-----------|-------------|----------| +| sam-react-prod | React 빌드+배포 | develop, stage, main | 개발/Stage/운영 | +| sam-api | Laravel API 배포 | stage, main | Stage/운영 | +| sam-manage | Laravel Admin 배포 | main | 운영 | +| sam-sales | 레거시 PHP 배포 | main | 운영 | + +### 브랜치별 동작 + +| 브랜치 | react | api | mng | sales | +|--------|-------|-----|-----|-------| +| develop | Jenkins 빌드 -> 개발서버 | 기존 hook | 기존 hook | 기존 hook | +| stage | Jenkins 빌드 -> 운영 Stage | Jenkins -> 운영 Stage | - | - | +| main | Jenkins 빌드 -> 운영 Production | Jenkins -> 운영 Production | Jenkins -> 운영 Production | Jenkins -> 운영 pull | + +**main 브랜치 배포:** 배포관리자가 CI/CD Gitea에 수동 push 후 자동 실행 + +```bash +# 1회 remote 등록 +git remote add production https://git.sam.it.kr/SamProject/sam-react-prod.git + +# 운영 배포 시 +git push production main +``` + +--- + +## 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/stage/main) + ▼ +개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자의 origin + │ + ├─ develop push 시 + │ ├─ api/mng/sales: 기존 post-update hook (개발서버 pull) ← 현행 유지 + │ └─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 개발서버 배포 + │ + ├─ stage push 시 + │ ├─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 운영서버 Stage 배포 + │ └─ api: hook → CI/CD Gitea push → Jenkins → 운영서버 Stage pull + │ + └─ main push 시 (react/mng/api) + └─ ❌ CI/CD Gitea에 자동 push 안함 + → 배포관리자가 수동으로 CI/CD Gitea에 push + → Jenkins 자동 배포 + +별도 처리: + sales/www(landing): hook → CI/CD Gitea → Jenkins → 운영서버 pull +``` + +### 브랜치별 배포 정책 상세 + +| 브랜치 | 저장소 | CI/CD Gitea 동기화 | Jenkins 배포 | 배포 대상 | +|--------|--------|-------------------|-------------|----------| +| **stage** | react | 자동 (hook) | 빌드 + rsync | 운영서버 Stage | +| **stage** | api | 자동 (hook) | SSH pull | 운영서버 Stage | +| **main** | react | 수동 (배포관리자) | 빌드 + rsync | 운영서버 Production | +| **main** | mng | 수동 (배포관리자) | SSH deploy | 운영서버 Production | +| **main** | api | 수동 (배포관리자) | SSH deploy | 운영서버 Production | +| **main** | sales | 자동 (hook) | SSH pull | 운영서버 Production | +| **main** | www | 자동 (hook) | SSH pull | 운영서버 Production | +| **develop** | react | 자동 (hook) | 빌드 → 개발서버 배포 | 개발서버 | +| **develop** | api/mng/sales | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | + +### post-receive hook 동기화 요약 + +| 저장소 | hook 대상 브랜치 | 동작 | +|--------|-----------------|------| +| sam-react-prod | stage, develop | CI/CD Gitea에 push | +| sam-api | stage | CI/CD Gitea에 push | +| sam-sales | main | CI/CD Gitea에 push | +| sam-landing | main | CI/CD Gitea에 push | +| sam-manage | ❌ 없음 | main만 사용, 배포관리자 수동 push | + +hook 스크립트 경로: `/data/GIT/samproject/.git/hooks/post-receive.d/push-to-cicd` +토큰 환경변수: `/data/GIT/.cicd-env` (chmod 600, owner: git) + +### Webhook 설정 (CI/CD Gitea → Jenkins) + +각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI): + +``` +Repository Settings → Webhooks → Add Webhook (Gitea) +- URL: https://ci.sam.it.kr/gitea-webhook/post +- Content Type: application/json +- Secret: +- Events: Push events +``` + +--- + +## 배포 흐름도 + +``` +개발자 로컬 + │ git push origin (develop / stage / 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) │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ stage push ──────────────────────────────────────────┐ │ +│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ +│ │ → rsync → 운영서버 Stage + PM2 reload │ │ +│ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 Stage Release + 심링크 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ main push (sales/www만 자동) ────────────────────────┐ │ +│ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 rsync │ │ +│ │ www → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 pull │ │ +│ │ react/mng/api → ❌ 자동 push 안함 │ │ +│ └───────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────┘ + +┌─ 운영 배포 (main - react/mng/api) ──────────────────────────┐ +│ │ +│ 배포관리자 로컬 │ +│ │ git push production main (CI/CD Gitea remote) │ +│ ▼ │ +│ CI/CD Gitea (git.sam.it.kr) │ +│ │ Webhook │ +│ ▼ │ +│ Jenkins → 운영서버 배포 │ +│ react: CI/CD 빌드 → rsync → PM2 reload │ +│ api: Release + 심링크 → PHP-FPM reload │ +│ mng: Release + 심링크 → PHP-FPM reload │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +### 환경별 배포 비교 + +| 항목 | 운영 (main) | Stage (stage) | 개발 (develop) | +|------|------------|---------------|----------------| +| **트리거** | 배포관리자 수동 push | 자동 (hook) | react만 자동 (hook), 나머지 기존 hook | +| **react 전략** | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | +| **api 전략** | Release + 심링크 | Release + 심링크 | 기존 post-update (pull) | +| **mng 전략** | 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 | 자동 (hook) | +| stage | 운영서버 (211.117.60.189) | /home/webservice/react-stage/releases/ | sam-front-stage | 자동 (hook) | +| main | 운영서버 (211.117.60.189) | /home/webservice/react/releases/ | sam-front | 수동 push | + +**환경변수 파일 (CI/CD 서버):** /var/lib/jenkins/env-files/react/ + +| 파일 | API URL | Frontend URL | +|------|---------|-------------| +| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | +| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | +| .env.main | https://api.sam.it.kr | https://sam.it.kr | + +**rsync 주의:** trailing slash 사용 금지: `.next` (O), `.next/` (X) + +**릴리즈 보관:** 운영 5개, Stage 3개 + +### Jenkinsfile (react/Jenkinsfile) + +```groovy +pipeline { + agent any + + environment { + DEPLOY_USER = 'hskwon' + RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Prepare Env') { + steps { + script { + def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" + sh "cp ${envFile} .env.local" + } + } + } + + stage('Install') { + steps { sh 'npm install --prefer-offline' } + } + + stage('Build') { + steps { sh 'npm run build' } + } + + // ── develop → 개발서버 배포 ── + stage('Deploy Development') { + when { branch 'develop' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + rsync -az --delete \ + --exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \ + .next package.json next.config.ts public node_modules \ + ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ + scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local + ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' + """ + } + } + } + + // ── stage → 운영서버 Stage 배포 ── + stage('Deploy Stage') { + when { branch 'stage' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' + rsync -az --delete \ + .next package.json next.config.ts public node_modules \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ + scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && + cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && + cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── main → 운영서버 Production 배포 ── + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' + rsync -az --delete \ + .next package.json next.config.ts public node_modules \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ + scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && + cd /home/webservice && pm2 reload sam-front && + cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + } + + post { + success { echo '✅ react 배포 완료 (' + env.BRANCH_NAME + ')' } + failure { echo '❌ react 배포 실패 (' + env.BRANCH_NAME + ')' } + } +} +``` + +### 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 +-> SSH: git clone -> composer install -> artisan cache -> migrate -> 심링크 전환 -> PHP-FPM reload +``` + +**브랜치별 배포 대상:** + +| 브랜치 | 대상 서버 | 대상 경로 | 트리거 | +|--------|----------|----------|--------| +| stage | 운영서버 | /home/webservice/api-stage/releases/ | 자동 (hook) | +| main | 운영서버 | /home/webservice/api/releases/ | 수동 push | +| develop | 개발서버 | - (기존 post-update hook) | 기존 hook | + +### Jenkinsfile (api/Jenkinsfile) + +```groovy +pipeline { + agent any + + environment { + DEPLOY_USER = 'hskwon' + APP_NAME = 'api' + RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + cd /home/webservice/api/releases && + git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && + ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage && + ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env && + cd /home/webservice/api/releases/${RELEASE_ID} && + composer install --no-dev --optimize-autoloader --no-interaction && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + php artisan migrate --force && + ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current && + sudo systemctl reload php8.4-fpm && + sudo supervisorctl restart sam-queue-worker:* && + cd /home/webservice/api/releases && + ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── stage → 운영서버 Stage ── + stage('Deploy Stage') { + when { branch 'stage' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + cd /home/webservice/api-stage/releases && + git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && + ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/${RELEASE_ID}/storage && + ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/${RELEASE_ID}/.env && + cd /home/webservice/api-stage/releases/${RELEASE_ID} && + composer install --no-dev --optimize-autoloader --no-interaction && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + php artisan migrate --force && + ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current && + sudo systemctl reload php8.4-fpm && + cd /home/webservice/api-stage/releases && + ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // develop → Jenkins 관여 안함 (기존 post-update hook 유지) + } + + post { + success { echo "✅ api 배포 완료 (${env.BRANCH_NAME})" } + failure { + echo "❌ api 배포 실패 (${env.BRANCH_NAME})" + script { + if (env.BRANCH_NAME in ['main', 'stage']) { + def baseDir = env.BRANCH_NAME == 'main' + ? '/home/webservice/api' + : '/home/webservice/api-stage' + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + PREV=\$(ls -1dt ${baseDir}/releases/*/ | sed -n "2p" | xargs basename) && + [ -n "\$PREV" ] && ln -sfn ${baseDir}/releases/\$PREV ${baseDir}/current && + sudo systemctl reload php8.4-fpm + ' + """ + } + } + } + } + } +} +``` + +### 수동 배포 절차 (API Production) + +```bash +ssh sam-prod + +# 1. 새 릴리즈 디렉토리 생성 +RELEASE_ID=$(date +%Y%m%d_%H%M%S) +cd /home/webservice/api/releases +git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git $RELEASE_ID + +# 2. shared 심링크 연결 +ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/$RELEASE_ID/storage +ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/$RELEASE_ID/.env + +# 3. 의존성 설치 +cd /home/webservice/api/releases/$RELEASE_ID +composer install --no-dev --optimize-autoloader --no-interaction + +# 4. 캐시 생성 +php artisan config:cache +php artisan route:cache +php artisan view:cache + +# 5. 마이그레이션 (필요시) +php artisan migrate --force + +# 6. 심링크 전환 (이 시점에 배포 적용) +ln -sfn /home/webservice/api/releases/$RELEASE_ID /home/webservice/api/current + +# 7. 서비스 리로드 +sudo systemctl reload php8.4-fpm +sudo supervisorctl restart sam-queue-worker:* + +# 8. 오래된 릴리즈 정리 (최근 5개만 유지) +cd /home/webservice/api/releases +ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true +``` + +### 수동 배포 절차 (API Stage) + +```bash +ssh sam-prod + +RELEASE_ID=$(date +%Y%m%d_%H%M%S) +cd /home/webservice/api-stage/releases +git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git $RELEASE_ID + +ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/$RELEASE_ID/storage +ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/$RELEASE_ID/.env + +cd /home/webservice/api-stage/releases/$RELEASE_ID +composer install --no-dev --optimize-autoloader --no-interaction +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan migrate --force + +ln -sfn /home/webservice/api-stage/releases/$RELEASE_ID /home/webservice/api-stage/current +sudo systemctl reload php8.4-fpm + +# 최근 3개만 유지 +cd /home/webservice/api-stage/releases +ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true +``` + +--- + +## MNG (Laravel Admin) 배포 + +API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요. + +```bash +ssh sam-prod + +RELEASE_ID=$(date +%Y%m%d_%H%M%S) +cd /home/webservice/mng/releases +git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.git $RELEASE_ID + +ln -sfn /home/webservice/mng/shared/storage /home/webservice/mng/releases/$RELEASE_ID/storage +ln -sfn /home/webservice/mng/shared/.env /home/webservice/mng/releases/$RELEASE_ID/.env + +cd /home/webservice/mng/releases/$RELEASE_ID +composer install --no-dev --optimize-autoloader --no-interaction + +# Vite 빌드 (Blade + Tailwind) +npm install --production=false +npm run build + +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan migrate --force + +ln -sfn /home/webservice/mng/releases/$RELEASE_ID /home/webservice/mng/current +sudo systemctl reload php8.4-fpm + +# 오래된 릴리즈 정리 +cd /home/webservice/mng/releases +ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true +``` + +--- + +## Sales (Plain PHP) 배포 + +레거시 PHP 애플리케이션. rsync 기반 배포. + +### Jenkinsfile (sales/Jenkinsfile) + +```groovy +pipeline { + agent any + environment { DEPLOY_USER = 'hskwon' } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + rsync -az --delete \ + --exclude='.git' --exclude='.env' --exclude='storage' \ + . ${DEPLOY_USER}@211.117.60.189:/home/webservice/sales/ + ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/sales && echo "sales deployed"' + """ + } + } + } + // develop → 개발서버는 기존 post-update hook 유지 + } + + post { + success { echo '✅ sales 배포 완료 (' + env.BRANCH_NAME + ')' } + failure { echo '❌ sales 배포 실패 (' + env.BRANCH_NAME + ')' } + } +} +``` + +### 수동 배포 + +```bash +ssh sam-prod +cd /home/webservice/sales +git pull origin main +``` + +별도 캐시나 빌드 절차 없음. .env 변경 시에만 주의. + +--- + +## Landing (정적 페이지) 배포 + +### Jenkinsfile (landing/Jenkinsfile) + +```groovy +pipeline { + agent any + environment { DEPLOY_USER = 'hskwon' } + + stages { + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/landing && git pull origin main'" + } + } + } + } +} +``` + +--- + +## 롤백 + +### React 롤백 + +```bash +# 이전 릴리즈 확인 +ssh sam-prod "ls -lt /home/webservice/react/releases/" +ssh sam-prod "readlink /home/webservice/react/current" + +# 롤백 실행 +ssh sam-prod " + PREV=\$(ls -1dt /home/webservice/react/releases/*/ | sed -n '2p' | xargs basename) && + echo \"롤백 대상: \$PREV\" && + ln -sfn /home/webservice/react/releases/\$PREV /home/webservice/react/current && + cd /home/webservice && pm2 reload sam-front +" +``` + +### API 롤백 + +```bash +ssh sam-prod "ls -1dt /home/webservice/api/releases/*/" + +ssh sam-prod " + PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n '2p' | xargs basename) && + echo \"롤백 대상: \$PREV\" && + ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current && + sudo systemctl reload php8.4-fpm && + sudo supervisorctl restart sam-queue-worker:* +" +``` + +--- + +## Jenkins 장애 시 수동 배포 + +### React 수동 배포 + +```bash +# CI/CD 서버에서 빌드 +cd /tmp +git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-react-prod.git react-build +cd react-build +cp /var/lib/jenkins/env-files/react/.env.main .env.local +npm install --prefer-offline +npm run build + +RELEASE_ID=$(date +%Y%m%d_%H%M%S) + +# 운영서버로 전송 +ssh sam-prod "mkdir -p /home/webservice/react/releases/${RELEASE_ID}" +rsync -az --delete \ + .next package.json next.config.ts public node_modules \ + hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ +scp .env.local hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local + +# 심링크 전환 및 PM2 재시작 +ssh sam-prod " + ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && + cd /home/webservice && pm2 reload sam-front +" + +# 빌드 디렉토리 정리 +rm -rf /tmp/react-build +``` + +### API 수동 배포 + +```bash +RELEASE_ID=$(date +%Y%m%d_%H%M%S) + +ssh sam-prod " + cd /home/webservice/api/releases && + git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && + ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage && + ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env && + cd /home/webservice/api/releases/${RELEASE_ID} && + composer install --no-dev --optimize-autoloader --no-interaction && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + php artisan migrate --force && + ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current && + sudo systemctl reload php8.4-fpm && + sudo supervisorctl restart sam-queue-worker:* +" +``` + +--- + +## 배포 후 확인 사항 + +```bash +# 서비스 상태 +sudo systemctl status nginx php8.4-fpm +pm2 status +sudo supervisorctl status + +# 에러 로그 +sudo tail -20 /var/log/nginx/api.sam.it.kr.error.log +sudo tail -20 /home/webservice/api/shared/storage/logs/laravel.log + +# HTTP 응답 확인 +curl -sI https://api.sam.it.kr +curl -sI https://sam.it.kr +curl -sI https://admin.codebridge-x.com +``` + +--- + +## 빌드 아티팩트 관리 + +```bash +# Jenkins workspace 용량 확인 +sudo du -sh /var/lib/jenkins/workspace/* + +# 운영서버 릴리즈 정리 +ssh sam-prod "cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf" +ssh sam-prod "cd /home/webservice/api/releases && ls -1dt */ | tail -n +6 | xargs rm -rf" + +# Jenkins 빌드 보관 정책: Jenkins > Job > Configure > Discard old builds +``` + +--- + +## 빌드 실패 조사 + +```bash +# Jenkins 로그에서 최근 오류 +sudo journalctl -u jenkins --since "30 minutes ago" | grep -i error + +# Jenkins workspace 확인 +ls -la /var/lib/jenkins/workspace/ + +# 웹 콘솔 로그 (권장) +# https://ci.sam.it.kr/job///console +``` + +**빌드 실패 주요 원인:** + +1. npm install 실패 -- node_modules 캐시, 네트워크 +2. npm run build 실패 -- TypeScript 오류, 환경변수 누락 +3. rsync 실패 -- SSH 키 문제, 디스크 공간 부족 +4. composer install 실패 -- 네트워크, PHP 확장 누락 +5. SSH 연결 실패 -- known_hosts 변경, 키 만료 \ No newline at end of file diff --git a/deploys/ops-manual/06-database.md b/deploys/ops-manual/06-database.md new file mode 100644 index 0000000..148f917 --- /dev/null +++ b/deploys/ops-manual/06-database.md @@ -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로 중단 +``` diff --git a/deploys/ops-manual/07-monitoring.md b/deploys/ops-manual/07-monitoring.md new file mode 100644 index 0000000..f64233d --- /dev/null +++ b/deploys/ops-manual/07-monitoring.md @@ -0,0 +1,253 @@ +# 7. 모니터링 + +[목차로 돌아가기](./README.md) + +--- + +## 아키텍처 + +``` +운영서버 (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 + +--- + +## 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' +``` + +### 스크래핑 대상 추가 + +```bash +# 1. 설정 파일 편집 +sudo vim /etc/prometheus/prometheus.yml + +# 2. 새 대상 추가 예시 +# - job_name: 'sam-dev' +# static_configs: +# - targets: ['114.203.209.83:9100'] +# labels: +# server: 'development' + +# 3. 문법 검사 +promtool check config /etc/prometheus/prometheus.yml + +# 4. 서비스 리로드 +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 + +**권장 알림 규칙:** + +| 조건 | 임계값 | 설명 | +|------|--------|------| +| CPU 사용률 | > 90% (5분) | CPU 과부하 | +| 메모리 사용률 | > 85% | 메모리 부족 | +| 디스크 사용률 | > 80% | 디스크 공간 부족 | +| 서비스 다운 | target down > 1분 | 스크래핑 실패 | + +**알림 채널:** Grafana > Alerting > Contact points 에서 이메일, Slack 등 설정 + +--- + +## [운영] 성능 모니터링 + +### 메모리 사용량 분석 + +```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" # 재시작 이력 +``` diff --git a/deploys/ops-manual/08-troubleshooting.md b/deploys/ops-manual/08-troubleshooting.md new file mode 100644 index 0000000..2a6fe35 --- /dev/null +++ b/deploys/ops-manual/08-troubleshooting.md @@ -0,0 +1,522 @@ +# 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 조정. + +--- + +### 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 심링크 확인. + +--- + +## 공통 장애 + +### 디스크 공간 부족 + +**증상:** 서비스 오류. 로그 기록 실패. 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//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 업체 | (IDC 연락처 기입) | 서버 물리적 장애, 네트워크 | diff --git a/deploys/ops-manual/09-security.md b/deploys/ops-manual/09-security.md new file mode 100644 index 0000000..4334206 --- /dev/null +++ b/deploys/ops-manual/09-security.md @@ -0,0 +1,206 @@ +# 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 | + +### 공통 명령어 + +```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 +``` + +### 화이트리스트 설정 + +```bash +# /etc/fail2ban/jail.local +[DEFAULT] +ignoreip = 127.0.0.1/8 관리자_IP_1 관리자_IP_2 + +# 변경 후 +sudo systemctl restart fail2ban +``` + +--- + +## [운영] .env 파일 보안 + +```bash +# 권한 확인 (600이어야 함) +ls -la /home/webservice/api/shared/.env +ls -la /home/webservice/mng/shared/.env +ls -la /home/webservice/sales/.env + +# 권한 수정 +chmod 600 /home/webservice/api/shared/.env +chmod 600 /home/webservice/mng/shared/.env +chmod 600 /home/webservice/sales/.env +``` + +--- + +## [운영] 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 diff --git a/deploys/ops-manual/10-backup-recovery.md b/deploys/ops-manual/10-backup-recovery.md new file mode 100644 index 0000000..1f4578d --- /dev/null +++ b/deploys/ops-manual/10-backup-recovery.md @@ -0,0 +1,355 @@ +# 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) + +**자동 백업:** crontab 매일 03:00 실행 + +- 스크립트: /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 +``` + +--- + +## 전체 서버 복구 절차 + +### [운영] 복구 순서 + +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. Java 설치 +4. Gitea 설치 + 설정/저장소 복원 +5. Jenkins 설치 + jobs/credentials/env-files/SSH 키 복원 +6. Nginx 설치 + 사이트 설정 + SSL 인증서 발급 +7. Prometheus + node_exporter 설치 + 설정 복원 +8. Grafana 설치 + 대시보드 임포트 +9. fail2ban 설치 +10. Webhook 연결 확인 +11. 전체 서비스 동작 검증 + +상세: [서버 설치 가이드](./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://admin.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';" +``` + +**자동 시작 확인:** + +```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 서비스명 +``` diff --git a/deploys/ops-manual/11-server-setup.md b/deploys/ops-manual/11-server-setup.md new file mode 100644 index 0000000..ef87eeb --- /dev/null +++ b/deploys/ops-manual/11-server-setup.md @@ -0,0 +1,1018 @@ +# 11. 서버 설치 가이드 + +[목차로 돌아가기](./README.md) + +--- + +## [운영] 설치 순서 + +| 순서 | 항목 | 의존성 | +|------|------|--------| +| ① | OS 기본 셋팅 (UFW, 스왑, 타임존) | - | +| ② | MySQL 8.4 | ① | +| ③ | Redis 7.x | ① | +| ④ | Nginx + Certbot | ① | +| ⑤ | PHP 8.4 + Composer | ① | +| ⑥ | Supervisor (Queue Worker) | ⑤ | +| ⑦ | Laravel API 배포 (api, api-stage, mng) | ②③⑤ | +| ⑧ | Sales 배포 | ⑤ | +| ⑨ | Node.js 22 + PM2 (react, react-stage) | ① | +| ⑩ | SSL 인증서 (Let's Encrypt) | ④ | +| ⑪ | node_exporter | ① | +| ⑫ | fail2ban | ① | +| ⑬ | 최종 점검 | 전체 | + +--- + +### ① OS 기본 셋팅 + +```bash +# 시스템 업데이트 +sudo apt update && sudo apt upgrade -y + +# 기본 패키지 +sudo apt install -y curl wget git unzip vim htop net-tools + +# 타임존 +sudo timedatectl set-timezone Asia/Seoul + +# 스왑 4GB 설정 +sudo fallocate -l 4G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab + +# 스왑 최적화 +echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf +sudo sysctl -p + +# UFW 방화벽 +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw allow from 110.10.147.46 to any port 9100 # node_exporter (CI/CD만) +sudo ufw allow from 110.10.147.46 to any port 3306 # MySQL (CI/CD 백업용) +sudo ufw enable + +# webservice 사용자 그룹 생성 +sudo groupadd -f webservice +sudo usermod -aG webservice hskwon +sudo usermod -aG webservice www-data +sudo mkdir -p /home/webservice +sudo chown hskwon:webservice /home/webservice +sudo chmod 2775 /home/webservice # setgid +``` + +### ② MySQL 8.4 + +```bash +# mysql-apt-config deb로 repo 등록 +sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb +sudo DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.33-1_all.deb + +# GPG 키 만료 시 — Ubuntu keyserver에서 갱신 +sudo gpg --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C +sudo gpg --export B7B3B788A8D3785C | sudo tee /usr/share/keyrings/mysql-apt-config.gpg > /dev/null + +# 설치 +sudo apt update +sudo apt install -y mysql-server +``` + +**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): + +```ini +[mysqld] +innodb_buffer_pool_size = 2048M +innodb_log_file_size = 512M +innodb_flush_log_at_trx_commit = 2 +max_connections = 100 +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow.log +long_query_time = 2 +validate_password.policy = LOW +``` + +**DB 및 사용자:** + +```sql +-- 데이터베이스 (4개) +CREATE DATABASE sam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 앱 사용자 +CREATE USER 'codebridge'@'localhost' IDENTIFIED BY '<비밀번호>'; +GRANT ALL PRIVILEGES ON sam.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON sam_stage.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON sam_stat.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; + +-- 관리자 (auth_socket) +CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; +GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; + +-- CI/CD 서버 백업용 +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'; + +FLUSH PRIVILEGES; +``` + +### ③ Redis 7.x + +```bash +sudo apt install -y redis-server + +# /etc/redis/redis.conf 설정: +# bind 127.0.0.1 ::1 +# maxmemory 512mb +# maxmemory-policy allkeys-lru +# supervised systemd + +sudo systemctl enable redis-server +sudo systemctl restart redis-server +redis-cli ping # → PONG +``` + +### ④ Nginx + Certbot + +```bash +sudo apt install -y nginx certbot python3-certbot-nginx +``` + +**보안 스니펫** (`/etc/nginx/snippets/security.conf`): + +```nginx +# 숨김 파일 차단 (.env, .git 등) +location ~ /\. { + deny all; + access_log off; + log_not_found off; +} + +# 환경설정/백업/로그 파일 차단 +location ~* \.(env|ini|log|conf|bak|sql)$ { + deny all; + access_log off; + log_not_found off; +} + +# Composer, Node 패키지 등 민감 파일 차단 +location ~* /(composer\.(json|lock)|package\.json|yarn\.lock|phpunit\.xml|artisan|server\.php)$ { + deny all; + access_log off; + log_not_found off; +} +``` + +**기본 설정** (`/etc/nginx/nginx.conf`): + +```nginx +worker_processes auto; +events { + worker_connections 1024; +} +http { + keepalive_timeout 65; + client_max_body_size 50M; + gzip on; + gzip_types text/plain application/json application/javascript text/css; +} +``` + +### ⑤ PHP 8.4 + Composer + +```bash +sudo add-apt-repository ppa:ondrej/php -y +sudo apt update + +sudo apt install -y \ + php8.4-fpm php8.4-mysql php8.4-mbstring php8.4-xml \ + php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath \ + php8.4-intl php8.4-redis php8.4-opcache + +curl -sS https://getcomposer.org/installer | php +sudo mv composer.phar /usr/local/bin/composer +``` + +**PHP-FPM Pool 설정 (4개):** + +| Pool | 설정 파일 | 소켓 | max_children | +|------|----------|------|-------------| +| api | /etc/php/8.4/fpm/pool.d/api.conf | php8.4-fpm-api.sock | 10 | +| admin | /etc/php/8.4/fpm/pool.d/admin.conf | php8.4-fpm-admin.sock | 5 | +| sales | /etc/php/8.4/fpm/pool.d/sales.conf | php8.4-fpm-sales.sock | 3 | +| api-stage | /etc/php/8.4/fpm/pool.d/api-stage.conf | php8.4-fpm-api-stage.sock | 3 | + +**Pool 설정 템플릿 (api.conf 예시):** + +```ini +[api] +user = www-data +group = webservice +listen = /run/php/php8.4-fpm-api.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 10 +pm.start_servers = 4 +pm.min_spare_servers = 2 +pm.max_spare_servers = 6 +pm.max_requests = 500 +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[post_max_size] = 50M +php_admin_value[display_errors] = Off +``` + +```bash +# 기본 pool 제거, 분리된 pool 사용 +sudo rm /etc/php/8.4/fpm/pool.d/www.conf +sudo systemctl restart php8.4-fpm +``` + +### ⑥ Supervisor (Queue Worker) + +```bash +sudo apt install -y supervisor + +sudo tee /etc/supervisor/conf.d/sam-queue.conf > /dev/null << 'EOF' +[program:sam-queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=2 +redirect_stderr=true +stdout_logfile=/home/webservice/api/shared/storage/logs/queue-worker.log +stopwaitsecs=3600 +EOF + +sudo supervisorctl reread +sudo supervisorctl update +``` + +### ⑦ Laravel 배포 (API / API-Stage / MNG) + +**디렉토리 구조 생성:** + +```bash +# API 운영 +sudo mkdir -p /home/webservice/api/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/api +sudo chmod -R 2775 /home/webservice/api + +# API Stage +sudo mkdir -p /home/webservice/api-stage/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/api-stage +sudo chmod -R 2775 /home/webservice/api-stage + +# MNG (Admin) +sudo mkdir -p /home/webservice/mng/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/mng +sudo chmod -R 2775 /home/webservice/mng +``` + +**초기 배포 절차:** + +```bash +# shared 심링크 연결 +ln -sfn /home/webservice/api/shared/storage /home/webservice/api/current/storage +ln -sfn /home/webservice/api/shared/.env /home/webservice/api/current/.env + +# 의존성 설치 + 최적화 +cd /home/webservice/api/current +composer install --no-dev --optimize-autoloader +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan migrate --force +``` + +### ⑧ Sales 배포 + +```bash +sudo mkdir -p /home/webservice/sales +sudo chown -R hskwon:webservice /home/webservice/sales + +cd /home/webservice +git clone sales +cp /home/webservice/sales/.env.example /home/webservice/sales/.env +chmod 600 /home/webservice/sales/.env +chmod 775 /home/webservice/sales/uploads +``` + +### ⑨ Node.js 22 + PM2 + +```bash +# Node.js 22 LTS +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs + +# PM2 설치 +sudo npm install -g pm2 + +# 운영 + Stage 디렉토리 +sudo mkdir -p /home/webservice/react/{releases,shared} +sudo mkdir -p /home/webservice/react-stage/{releases,shared} +sudo chown -R hskwon:webservice /home/webservice/react /home/webservice/react-stage +``` + +**PM2 설정** (`/home/webservice/ecosystem.config.js`): + +```javascript +module.exports = { + apps: [ + { + name: 'sam-front', + cwd: '/home/webservice/react/current', + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3000', + instances: 2, + exec_mode: 'cluster', + max_memory_restart: '300M', + env: { + NODE_ENV: 'production', + NODE_OPTIONS: '--max-old-space-size=256' + } + }, + { + name: 'sam-front-stage', + cwd: '/home/webservice/react-stage/current', + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3100', + instances: 1, + exec_mode: 'fork', + max_memory_restart: '200M', + env: { + NODE_ENV: 'production', + NODE_OPTIONS: '--max-old-space-size=128' + } + } + ] +}; +``` + +```bash +cd /home/webservice +pm2 start ecosystem.config.js +pm2 save +pm2 startup +``` + +### ⑩ SSL 인증서 + +```bash +# Nginx 사이트 활성화 +sudo ln -s /etc/nginx/sites-available/sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/api.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/admin.codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/sales.codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/stage.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/stage-api.sam.it.kr /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +# SSL 인증서 발급 +sudo certbot --nginx -d sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d api.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d stage.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d stage-api.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d admin.codebridge-x.com --email develop@codebridge-x.com +sudo certbot --nginx -d sales.codebridge-x.com --email develop@codebridge-x.com +sudo certbot --nginx -d codebridge-x.com -d www.codebridge-x.com --email develop@codebridge-x.com + +# 자동 갱신 확인 +sudo certbot renew --dry-run +``` + +### ⑪ node_exporter + +```bash +cd /tmp +wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz +tar xzf node_exporter-1.8.2.linux-amd64.tar.gz +sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/ +rm -rf node_exporter-1.8.2* + +sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF' +[Unit] +Description=Node Exporter +After=network.target + +[Service] +User=nobody +ExecStart=/usr/local/bin/node_exporter +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable node_exporter +sudo systemctl start node_exporter +``` + +### ⑫ fail2ban + +```bash +sudo apt install -y fail2ban +sudo systemctl enable fail2ban +sudo systemctl start fail2ban +``` + +--- + +## Nginx 사이트 설정 템플릿 + +### sam.it.kr (Next.js 운영) + +```nginx +upstream nextjs_prod { + server 127.0.0.1:3000; + keepalive 32; +} + +server { + listen 80; + server_name sam.it.kr; + + access_log /var/log/nginx/sam.it.kr.access.log; + error_log /var/log/nginx/sam.it.kr.error.log; + + location /_next/static/ { + alias /home/webservice/react/current/.next/static/; + expires 365d; + add_header Cache-Control "public, immutable"; + } + + location / { + proxy_pass http://nextjs_prod; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} +``` + +### api.sam.it.kr (Laravel API) + +```nginx +server { + listen 80; + server_name api.sam.it.kr; + + root /home/webservice/api/current/public; + index index.php; + + access_log /var/log/nginx/api.sam.it.kr.access.log; + error_log /var/log/nginx/api.sam.it.kr.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-api.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + client_max_body_size 50M; +} +``` + +### admin.codebridge-x.com (Laravel Admin) + +```nginx +server { + listen 80; + server_name admin.codebridge-x.com; + + root /home/webservice/mng/current/public; + index index.php; + + access_log /var/log/nginx/admin.codebridge-x.com.access.log; + error_log /var/log/nginx/admin.codebridge-x.com.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-admin.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + client_max_body_size 50M; +} +``` + +### sales.codebridge-x.com (Plain PHP) + +```nginx +server { + listen 80; + server_name sales.codebridge-x.com; + + root /home/webservice/sales; + index index.php index.html; + + access_log /var/log/nginx/sales.codebridge-x.com.access.log; + error_log /var/log/nginx/sales.codebridge-x.com.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-sales.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + # uploads PHP 실행 차단 (보안) + location ~* /uploads/.*\.php$ { + deny all; + } + + client_max_body_size 50M; +} +``` + +### stage.sam.it.kr / stage-api.sam.it.kr + +stage.sam.it.kr은 sam.it.kr과 동일 구조 (upstream 포트: 3100). +stage-api.sam.it.kr은 api.sam.it.kr과 동일 구조 (소켓: php8.4-fpm-api-stage.sock, root: api-stage). + +--- + +## [CI/CD] 설치 순서 + +| 순서 | 항목 | 의존성 | +|------|------|--------| +| ① | OS 기본 셋팅 (UFW, 스왑, 타임존) | - | +| ② | MySQL 8.4 | ① | +| ③ | Java 17 (Jenkins 런타임) | ① | +| ④ | Gitea | ② | +| ⑤ | 개발서버 post-receive hook 설정 | ④ | +| ⑥ | Jenkins | ③ | +| ⑦ | Nginx + SSL | ④⑥ | +| ⑧ | Prometheus + node_exporter | ① | +| ⑨ | Grafana | ⑧ | +| ⑩ | Jenkins 파이프라인 + Webhook | ⑥⑦ | +| ⑪ | 백업 자동화 | ② | +| ⑫ | fail2ban + 최종 점검 | 전체 | + +--- + +### ① OS 기본 셋팅 + +운영서버와 동일. 차이점: +- UFW: 22, 80, 443만 허용 (9100, 3306 불필요) +- webservice 그룹 생성 (배포 스크립트용) + +### ② MySQL 8.4 + +운영서버와 동일한 설치 방법. 튜닝 차이: + +```ini +[mysqld] +innodb_buffer_pool_size = 1536M # 운영(2048M)보다 작음 +innodb_redo_log_capacity = 536870912 +innodb_flush_log_at_trx_commit = 2 +max_connections = 50 # 운영(100)보다 작음 +``` + +**DB 및 사용자:** + +```sql +-- Gitea DB +CREATE DATABASE gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'gitea'@'localhost' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'localhost'; + +-- 관리자 +CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; +GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; + +FLUSH PRIVILEGES; +``` + +### ③ Java 17 + +```bash +sudo apt install -y openjdk-17-jre-headless +java -version +``` + +### ④ Gitea + +```bash +GITEA_VERSION="1.22.6" +wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64 +sudo mv /tmp/gitea /usr/local/bin/gitea +sudo chmod +x /usr/local/bin/gitea + +sudo adduser --system --group --disabled-password --shell /bin/bash --home /home/git git + +sudo mkdir -p /var/lib/gitea/{custom,data,log} +sudo mkdir -p /etc/gitea +sudo chown -R git:git /var/lib/gitea +sudo chown git:git /etc/gitea +sudo chmod 750 /etc/gitea +``` + +**systemd 서비스:** + +```ini +# /etc/systemd/system/gitea.service +[Unit] +Description=Gitea (Git with a cup of tea) +After=syslog.target network.target mysql.service + +[Service] +Type=simple +User=git +Group=git +WorkingDirectory=/var/lib/gitea/ +ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +Restart=always +Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +**Gitea 설정** (`/etc/gitea/app.ini`): + +```ini +[server] +DOMAIN = git.sam.it.kr +HTTP_PORT = 3000 +ROOT_URL = https://git.sam.it.kr/ +DISABLE_SSH = false +SSH_PORT = 22 +LFS_START_SERVER = true + +[database] +DB_TYPE = mysql +HOST = 127.0.0.1:3306 +NAME = gitea +USER = gitea +PASSWD = +CHARSET = utf8mb4 + +[repository] +ROOT = /var/lib/gitea/data/repositories + +[log] +ROOT_PATH = /var/lib/gitea/log +MODE = file +LEVEL = info + +[service] +DISABLE_REGISTRATION = true +REQUIRE_SIGNIN_VIEW = true +``` + +**초기 설정:** +1. https://git.sam.it.kr 웹 설치 마법사 완료 +2. 관리자 계정 생성 (hskwon) +3. Organization "SamProject" 생성 +4. 저장소 생성: sam-api, sam-manage, sam-react-prod, sam-sales, sam-landing +5. Jenkins Webhook용 API 토큰 생성 + +### ⑤ 개발서버 post-receive hook (선택적 브랜치 동기화) + +**토큰 보안 파일 (개발서버):** + +```bash +# /data/GIT/.cicd-env (chmod 600, owner: git) +CICD_GITEA_TOKEN=<토큰> +CICD_GITEA_USER=hskwon +CICD_GITEA_HOST=git.sam.it.kr +``` + +**hook 스크립트** (`/data/GIT/samproject/.git/hooks/post-receive.d/push-to-cicd`): + +```bash +#!/bin/bash +source /data/GIT/.cicd-env +LOGFILE=/home/webservice/logs/cicd_push_.log +CICD_REMOTE="https://${CICD_GITEA_USER}:${CICD_GITEA_TOKEN}@${CICD_GITEA_HOST}/SamProject/.git" + +mkdir -p /home/webservice/logs + +while read oldrev newrev refname; do + BRANCH=$(echo "$refname" | sed 's|refs/heads/||') + if [ "$BRANCH" = "" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S'): Pushing ${BRANCH} to CI/CD Gitea" >> $LOGFILE + git push $CICD_REMOTE ${BRANCH}:${BRANCH} >> $LOGFILE 2>&1 + echo "$(date '+%Y-%m-%d %H:%M:%S'): Done (exit: $?)" >> $LOGFILE + fi +done +``` + +**동기화 요약:** + +| 저장소 | hook 대상 브랜치 | 동작 | +|--------|-----------------|------| +| sam-react-prod | stage, develop | CI/CD Gitea에 push | +| sam-api | stage | CI/CD Gitea에 push | +| sam-sales | main | CI/CD Gitea에 push | +| sam-landing | main | CI/CD Gitea에 push | +| sam-manage | ❌ 없음 | main만 사용, 배포관리자 수동 push | + +### ⑥ Jenkins + +```bash +# GPG 키 + APT Repository +sudo gpg --keyserver keyserver.ubuntu.com --recv-keys 7198F4B714ABFC68 +sudo gpg --export 7198F4B714ABFC68 | sudo tee /usr/share/keyrings/jenkins-keyring.gpg > /dev/null +echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.gpg]" \ + https://pkg.jenkins.io/debian-stable binary/ | sudo tee \ + /etc/apt/sources.list.d/jenkins.list > /dev/null + +sudo apt update +sudo apt install -y jenkins + +# JVM 메모리 제한 +sudo mkdir -p /etc/systemd/system/jenkins.service.d +sudo tee /etc/systemd/system/jenkins.service.d/override.conf > /dev/null << 'EOF' +[Service] +Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true" +EOF + +sudo systemctl daemon-reload +sudo systemctl enable jenkins +sudo systemctl start jenkins +``` + +**필요 도구 설치:** + +```bash +# Node.js 22 (react 빌드용) +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs + +# PHP 8.4 + Composer (선택 — Laravel 테스트용) +sudo add-apt-repository ppa:ondrej/php -y +sudo apt update +sudo apt install -y php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-mysql php8.4-bcmath +curl -sS https://getcomposer.org/installer | php +sudo mv composer.phar /usr/local/bin/composer +``` + +**SSH 키 설정:** + +```bash +sudo -u jenkins ssh-keygen -t ed25519 -C "jenkins@sam-cicd" -f /var/lib/jenkins/.ssh/id_ed25519 -N "" + +# 운영/개발 서버에 공개키 등록 +ssh sam-prod "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" +ssh sam-dev "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" + +# known_hosts 등록 +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 +``` + +**Jenkins Credentials:** +- `deploy-ssh-key`: SSH 키 (hskwon@운영/개발 서버 공용) +- `gitea-api-token`: Gitea API 토큰 + +### ⑦ Nginx + SSL (CI/CD) + +**리버스 프록시 설정:** + +```nginx +# /etc/nginx/sites-available/git.sam.it.kr +server { + listen 80; + server_name git.sam.it.kr; + client_max_body_size 500M; + proxy_request_buffering off; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# /etc/nginx/sites-available/ci.sam.it.kr +server { + listen 80; + server_name ci.sam.it.kr; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 90; + proxy_buffering off; + } +} + +# /etc/nginx/sites-available/monitor.sam.it.kr +server { + listen 80; + server_name monitor.sam.it.kr; + + location / { + proxy_pass http://127.0.0.1:3100; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +```bash +sudo ln -s /etc/nginx/sites-available/git.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/ci.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/monitor.sam.it.kr /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +sudo certbot --nginx -d git.sam.it.kr +sudo certbot --nginx -d ci.sam.it.kr +sudo certbot --nginx -d monitor.sam.it.kr +``` + +### ⑧ Prometheus + node_exporter + +```bash +# Prometheus +PROM_VERSION="2.51.0" +cd /tmp +wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz +tar xzf prometheus-${PROM_VERSION}.linux-amd64.tar.gz +sudo mv prometheus-${PROM_VERSION}.linux-amd64/prometheus /usr/local/bin/ +sudo mv prometheus-${PROM_VERSION}.linux-amd64/promtool /usr/local/bin/ +sudo mkdir -p /etc/prometheus /var/lib/prometheus +sudo mv prometheus-${PROM_VERSION}.linux-amd64/consoles /etc/prometheus/ +sudo mv prometheus-${PROM_VERSION}.linux-amd64/console_libraries /etc/prometheus/ +rm -rf prometheus-${PROM_VERSION}* +sudo useradd --no-create-home --shell /bin/false prometheus +sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus +``` + +**systemd 서비스:** + +```ini +# /etc/systemd/system/prometheus.service +[Unit] +Description=Prometheus +After=network-online.target + +[Service] +User=prometheus +Group=prometheus +Type=simple +ExecStart=/usr/local/bin/prometheus \ + --config.file=/etc/prometheus/prometheus.yml \ + --storage.tsdb.path=/var/lib/prometheus/ \ + --storage.tsdb.retention.time=30d \ + --web.listen-address=127.0.0.1:9090 +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +node_exporter: 운영서버 설치와 동일. + +```bash +sudo systemctl daemon-reload +sudo systemctl enable prometheus node_exporter +sudo systemctl start prometheus node_exporter +``` + +### ⑨ Grafana + +```bash +sudo mkdir -p /etc/apt/keyrings/ +wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null +echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list +sudo apt update +sudo apt install -y grafana +``` + +**설정** (`/etc/grafana/grafana.ini`): + +```ini +[server] +http_port = 3100 +domain = monitor.sam.it.kr +root_url = https://monitor.sam.it.kr/ + +[security] +admin_password = + +[users] +allow_sign_up = false +``` + +```bash +sudo systemctl enable grafana-server +sudo systemctl start grafana-server +``` + +**초기 설정:** Data Source: Prometheus (http://localhost:9090) → 대시보드 임포트: Node Exporter Full (ID: 1860) + +--- + +## 보안 체크리스트 + +### [운영] + +- [x] SSH 키 인증만 허용 (비밀번호 로그인 비활성화) +- [x] root SSH 로그인 비활성화 +- [x] UFW 방화벽 활성화 +- [x] MySQL root 원격 접근 차단 (auth_socket) +- [x] MySQL 앱 사용자 최소 권한 (codebridge) +- [x] .env 파일 권한 600 (api, admin, sales) +- [x] storage/ 디렉토리 권한 775 +- [x] Nginx security.conf 스니펫 적용 +- [x] PHP display_errors = Off (모든 pool) +- [x] Laravel APP_DEBUG=false, APP_ENV=production +- [x] Sales uploads/ PHP 실행 차단 +- [x] Certbot 자동 갱신 (7/7 dry-run success) +- [x] fail2ban (SSH 브루트포스 방지) +- [x] Redis bind 127.0.0.1 (외부 접근 차단) +- [x] node_exporter CI/CD IP만 허용 (UFW) + +### [CI/CD] + +- [x] SSH 키 인증만 허용 (PasswordAuthentication no) +- [x] root SSH 로그인 비활성화 (PermitRootLogin no) +- [x] UFW 방화벽 활성화 (22, 80, 443만) +- [x] Jenkins 관리자 계정 변경 (hskwon) +- [x] Gitea 회원가입 비활성화 (DISABLE_REGISTRATION = true) +- [x] Grafana 익명 접근 비활성화 (allow_sign_up = false) +- [x] Prometheus 외부 접근 차단 (127.0.0.1:9090) +- [x] MySQL root 원격 접근 차단 (auth_socket) +- [x] fail2ban (sshd jail) +- [x] Certbot 자동 갱신 +- [x] Jenkins SSH 키 ed25519 + Credential 등록 +- [x] Webhook Secret 설정 (Gitea → Jenkins) +- [x] post-receive hook 토큰 보안 (600 권한) + +--- + +## 개발서버 비교 (참고) + +| 항목 | 개발서버 | 운영서버 | +|------|----------|---------| +| OS | Ubuntu 24.04.2 | Ubuntu 24.04 (kernel 6.8.0-100) | +| CPU/RAM | 2C / 3.8GB (스왑 없음) | 2C / 8GB + 스왑 4GB | +| PHP | 8.4.15 (+ 5.6, 7.3) | 8.4.18 | +| MySQL | **8.0.45** | **8.4.8** | +| Node.js | 22.17.1 | 22.17.1 | +| Nginx | 1.24.0 | 1.24.0 | +| Redis | - | 7.0.15 (512mb) | +| PHP-FPM | 단일 www pool | 4개 분리 (api/admin/sales/stage) | +| PM2 | fork ×1 (:3001) | cluster ×2 (:3000) + fork ×1 (:3100) | +| Supervisor | - | queue worker ×2 | +| UFW | **비활성** | 활성 | +| fail2ban | - | ✅ | diff --git a/deploys/ops-manual/README.md b/deploys/ops-manual/README.md new file mode 100644 index 0000000..5133736 --- /dev/null +++ b/deploys/ops-manual/README.md @@ -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) 참조 \ No newline at end of file diff --git a/deploys/production-server-setup.md b/deploys/production-server-setup.md deleted file mode 100644 index 108a23d..0000000 --- a/deploys/production-server-setup.md +++ /dev/null @@ -1,1110 +0,0 @@ -# 운영서버 셋팅 가이드 - -> 작성일: 2026-02-23 | 최종 수정: 2026-02-24 -> 상태: ✅ 완료 (커널 6.8.0-100, 전체 서비스 가동 중) - ---- - -## 1. 서버 구성 개요 - -### 인프라 구조 - -``` -┌──────────────────────────────────────────────────────────┐ -│ 운영서버 (2 vCPU / 8GB) │ -│ Ubuntu 24.04 / IDC 클라우드 │ -│ IP: 211.117.60.189 │ -│ Kernel: 6.8.0-100-generic │ -│ │ -│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │ -│ │ Nginx │ │ Certbot │ │ UFW (22,80,443,9100) │ │ -│ │ (Proxy) │ │ (SSL) │ │ │ │ -│ └────┬─────┘ └───────────┘ └───────────────────────┘ │ -│ │ │ -│ ┌────┴───────────────────────────────────────────────┐ │ -│ │ Virtual Hosts (운영) │ │ -│ │ │ │ -│ │ sam.it.kr ──────────→ Next.js (PM2 cluster, :3000)│ │ -│ │ api.sam.it.kr ──────→ PHP-FPM (api pool) │ │ -│ │ admin.codebridge-x.com → PHP-FPM (admin pool) │ │ -│ │ sales.codebridge-x.com → PHP-FPM (sales pool) │ │ -│ │ codebridge-x.com ──→ 정적 랜딩페이지 │ │ -│ │ │ │ -│ │ Virtual Hosts (Stage) │ │ -│ │ │ │ -│ │ 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서버 (2 vCPU / 8GB) │ -│ Ubuntu 24.04 / IDC 클라우드 │ -│ IP: 110.10.147.46 │ -│ │ -│ ┌──────────┐ ┌───────────┐ ┌────────────────────┐ │ -│ │ Jenkins │ │ Gitea │ │ MySQL 8.4 │ │ -│ │ │ │ │ │ (백업/슬레이브) │ │ -│ └──────────┘ └───────────┘ └────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ │ -│ │ Prometheus │ │ Grafana │ │ -│ │ (수집) │ │ (시각화) │ │ -│ └──────────────┘ └──────────────┘ │ -└───────────────────────────────────────────────────────────┘ -``` - -### 도메인 매핑 - -| 도메인 | 서비스 | 앱 | SSL | -|--------|--------|-----|-----| -| sam.it.kr | Next.js 15 (PM2 cluster) | react/ | Let's Encrypt | -| api.sam.it.kr | Laravel 12 API | api/ | Let's Encrypt | -| admin.codebridge-x.com | Laravel 12 Admin | mng/ | Let's Encrypt | -| sales.codebridge-x.com | Plain PHP (레거시) | sales/ | Let's Encrypt | -| codebridge-x.com (+ www) | 정적 랜딩페이지 | landing/ | Let's Encrypt | -| stage.sam.it.kr | Next.js 15 (PM2 fork) | react/ (stage) | Let's Encrypt | -| stage-api.sam.it.kr | Laravel 12 API (stage) | api/ (stage) | Let's Encrypt | - -### 도메인 환경 분리 - -| 서비스 | 운영 | 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 | admin.codebridge-x.com | - | mng.codebridge-x.com | -| Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com | -| Landing | codebridge-x.com | - | - | - -### SSH 접속 정보 - -| 별칭 | IP | 용도 | 설정완료 | -|------|-----|------|---------| -| sam-prod | 211.117.60.189 | 운영서버 | ✅ | -| sam-cicd | 110.10.147.46 | CI/CD서버 | ✅ | -| sam-dev | 114.203.209.83 | 개발서버 | ✅ | - -보안: root SSH 차단, 비밀번호 로그인 차단, hskwon 키 인증 + sudo NOPASSWD - ---- - -## 2. 메모리 배분 계획 (8GB) - -| 서비스 | 할당 | 설정 | 비고 | -|--------|------|------|------| -| MySQL 8.4 | ~2GB | innodb_buffer_pool_size=2G | 메인 DB | -| Redis | ~0.5GB | maxmemory 512mb | 캐시/세션/큐 | -| PHP-FPM (API) | ~0.8GB | pm.max_children=10 | 트래픽 주력 | -| PHP-FPM (Admin) | ~0.3GB | pm.max_children=5 | 관리자용 | -| PHP-FPM (Sales) | ~0.2GB | pm.max_children=3 | 영업자용 | -| PHP-FPM (API-Stage) | ~0.2GB | pm.max_children=3 | Stage | -| 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 | Laravel 큐 | -| Nginx | ~0.1GB | worker_connections 1024 | 리버스 프록시 | -| node_exporter | ~0.01GB | - | 모니터링 | -| OS + 여유 | ~2.9GB | 스왑 4GB | 안전 마진 | -| **합계** | **~8GB** | | **스왑 4GB 백업** | - ---- - -## 3. 설치 순서 - -### ① OS 기본 셋팅 ✅ (2026-02-23) - -```bash -# 시스템 업데이트 -sudo apt update && sudo apt upgrade -y - -# 기본 패키지 -sudo apt install -y curl wget git unzip vim htop net-tools - -# 타임존 -sudo timedatectl set-timezone Asia/Seoul - -# 스왑 4GB 설정 -sudo fallocate -l 4G /swapfile -sudo chmod 600 /swapfile -sudo mkswap /swapfile -sudo swapon /swapfile -echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab - -# 스왑 최적화 -echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf -sudo sysctl -p - -# UFW 방화벽 -sudo ufw default deny incoming -sudo ufw default allow outgoing -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS -sudo ufw allow from 110.10.147.46 to any port 9100 # node_exporter (CI/CD만) -sudo ufw allow from 110.10.147.46 to any port 3306 # MySQL (CI/CD 백업용) -sudo ufw enable - -# webservice 사용자 그룹 생성 -sudo groupadd -f webservice -sudo usermod -aG webservice hskwon -sudo usermod -aG webservice www-data -sudo mkdir -p /home/webservice -sudo chown hskwon:webservice /home/webservice -sudo chmod 2775 /home/webservice # setgid -``` - -### ② MySQL 8.4 ✅ (2026-02-23) - -> **설치 완료**: MySQL 8.4.8 (GPG 키 만료 이슈로 Ubuntu keyserver 경유 설치) - -**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): - -```ini -[mysqld] -innodb_buffer_pool_size = 2048M -innodb_log_file_size = 512M -innodb_flush_log_at_trx_commit = 2 -max_connections = 100 -character-set-server = utf8mb4 -collation-server = utf8mb4_unicode_ci -slow_query_log = 1 -slow_query_log_file = /var/log/mysql/slow.log -long_query_time = 2 -validate_password.policy = LOW -``` - -**DB 및 사용자:** - -```sql --- 데이터베이스 (4개) -CREATE DATABASE sam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- 앱 사용자 (codebridge) -CREATE USER 'codebridge'@'localhost' IDENTIFIED BY '<비밀번호>'; -GRANT ALL PRIVILEGES ON sam.* TO 'codebridge'@'localhost'; -GRANT ALL PRIVILEGES ON sam_stage.* TO 'codebridge'@'localhost'; -GRANT ALL PRIVILEGES ON sam_stat.* TO 'codebridge'@'localhost'; -GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; - --- 관리자 (hskwon) - auth_socket + 패스워드 인증 -CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; -GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; - --- CI/CD 서버 백업용 -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'; - -FLUSH PRIVILEGES; -``` - -> **참고**: root는 auth_socket 인증 (비밀번호 없이 `sudo mysql`로 접근) -> **MySQL 업그레이드 전략**: 운영(8.4) 안정화 확인 후 → 개발서버(8.0→8.4) 업그레이드 - -### ③ Redis 7.0.15 ✅ (2026-02-24) - -```bash -# Redis 설치 -sudo apt install -y redis-server - -# 설정 (/etc/redis/redis.conf) -# bind 127.0.0.1 ::1 -# maxmemory 512mb -# maxmemory-policy allkeys-lru -# supervised systemd - -sudo systemctl enable redis-server -sudo systemctl restart redis-server - -# 연결 테스트 -redis-cli ping # → PONG -``` - -**Laravel .env 설정:** -``` -REDIS_HOST=127.0.0.1 -REDIS_PORT=6379 -QUEUE_CONNECTION=redis -CACHE_STORE=redis -SESSION_DRIVER=redis -``` - -### ④ Nginx 1.24.0 + Certbot 2.9.0 ✅ (2026-02-24) - -```bash -# Nginx 설치 -sudo apt install -y nginx - -# Certbot 설치 -sudo apt install -y certbot python3-certbot-nginx - -# 보안 스니펫 생성 (개발서버와 동일) -sudo tee /etc/nginx/snippets/security.conf > /dev/null << 'SECEOF' -# 숨김 파일 차단 (.env, .git 등) -location ~ /\. { - deny all; - access_log off; - log_not_found off; -} - -# 환경설정/백업/로그 파일 차단 -location ~* \.(env|ini|log|conf|bak|sql)$ { - deny all; - access_log off; - log_not_found off; -} - -# Git 폴더 차단 -location ~ ^/\.git { - deny all; - access_log off; - log_not_found off; -} - -# Composer, Node 패키지 등 민감 파일 차단 -location ~* /(composer\.(json|lock)|package\.json|yarn\.lock|phpunit\.xml|artisan|server\.php)$ { - deny all; - access_log off; - log_not_found off; -} -SECEOF -``` - -**기본 설정 최적화** (`/etc/nginx/nginx.conf`): - -```nginx -worker_processes auto; -events { - worker_connections 1024; -} -http { - keepalive_timeout 65; - client_max_body_size 50M; - gzip on; - gzip_types text/plain application/json application/javascript text/css; -} -``` - -### ⑤ PHP 8.4.18 + Composer 2.9.5 ✅ (2026-02-24) - -```bash -# PHP 8.4 PPA -sudo add-apt-repository ppa:ondrej/php -y -sudo apt update - -# PHP 8.4 + 확장 -sudo apt install -y \ - php8.4-fpm \ - php8.4-mysql \ - php8.4-mbstring \ - php8.4-xml \ - php8.4-curl \ - php8.4-zip \ - php8.4-gd \ - php8.4-bcmath \ - php8.4-intl \ - php8.4-redis \ - php8.4-opcache - -# Composer 설치 -curl -sS https://getcomposer.org/installer | php -sudo mv composer.phar /usr/local/bin/composer -``` - -**PHP-FPM Pool 분리 (4개):** - -```ini -# /etc/php/8.4/fpm/pool.d/api.conf -[api] -user = www-data -group = webservice -listen = /run/php/php8.4-fpm-api.sock -listen.owner = www-data -listen.group = www-data -pm = dynamic -pm.max_children = 10 -pm.start_servers = 4 -pm.min_spare_servers = 2 -pm.max_spare_servers = 6 -pm.max_requests = 500 -php_admin_value[memory_limit] = 128M -php_admin_value[upload_max_filesize] = 50M -php_admin_value[post_max_size] = 50M -php_admin_value[display_errors] = Off -``` - -```ini -# /etc/php/8.4/fpm/pool.d/admin.conf -[admin] -user = www-data -group = webservice -listen = /run/php/php8.4-fpm-admin.sock -listen.owner = www-data -listen.group = www-data -pm = dynamic -pm.max_children = 5 -pm.start_servers = 2 -pm.min_spare_servers = 1 -pm.max_spare_servers = 3 -pm.max_requests = 500 -php_admin_value[memory_limit] = 128M -php_admin_value[upload_max_filesize] = 50M -php_admin_value[post_max_size] = 50M -php_admin_value[display_errors] = Off -``` - -```ini -# /etc/php/8.4/fpm/pool.d/sales.conf -[sales] -user = www-data -group = webservice -listen = /run/php/php8.4-fpm-sales.sock -listen.owner = www-data -listen.group = www-data -pm = dynamic -pm.max_children = 3 -pm.start_servers = 1 -pm.min_spare_servers = 1 -pm.max_spare_servers = 2 -pm.max_requests = 500 -php_admin_value[memory_limit] = 128M -php_admin_value[upload_max_filesize] = 50M -php_admin_value[post_max_size] = 50M -php_admin_value[display_errors] = Off -``` - -```ini -# /etc/php/8.4/fpm/pool.d/api-stage.conf -[api-stage] -user = www-data -group = webservice -listen = /run/php/php8.4-fpm-api-stage.sock -listen.owner = www-data -listen.group = www-data -pm = dynamic -pm.max_children = 3 -pm.start_servers = 1 -pm.min_spare_servers = 1 -pm.max_spare_servers = 2 -pm.max_requests = 500 -php_admin_value[memory_limit] = 128M -php_admin_value[upload_max_filesize] = 50M -php_admin_value[post_max_size] = 50M -php_admin_value[display_errors] = Off -``` - -```bash -# 기본 pool 제거, 분리된 pool 사용 -sudo rm /etc/php/8.4/fpm/pool.d/www.conf -sudo systemctl restart php8.4-fpm -``` - -### ⑥ Supervisor ✅ (2026-02-24) - -```bash -sudo apt install -y supervisor - -# Laravel Queue Worker 설정 -sudo tee /etc/supervisor/conf.d/sam-queue.conf > /dev/null << 'EOF' -[program:sam-queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=2 -redirect_stderr=true -stdout_logfile=/home/webservice/api/shared/storage/logs/queue-worker.log -stopwaitsecs=3600 -EOF - -sudo supervisorctl reread -sudo supervisorctl update -``` - -### ⑦ Laravel API 배포 (api.sam.it.kr) ✅ (2026-02-24) - -> **초기 배포**: 개발서버에서 tar pipe로 전체 소스 전송 (이미지/업로드 파일 포함) -> **이후 배포**: CI/CD Jenkins → Git pull + composer install - -```bash -# 디렉토리 구조 -sudo mkdir -p /home/webservice/api/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} -sudo chown -R hskwon:webservice /home/webservice/api -sudo chmod -R 2775 /home/webservice/api - -# 초기 배포 (개발서버 → 운영서버, tar pipe) -ssh sam-dev "cd /home/webservice && tar cf - --exclude='vendor' --exclude='node_modules' --exclude='.env' api/" | \ - ssh sam-prod "cd /home/webservice && tar xf -" -# 이후: CI/CD에서 git clone + releases 구조 사용 -ln -sfn /home/webservice/api/releases/20260223 /home/webservice/api/current - -# shared 심링크 -ln -sfn /home/webservice/api/shared/storage /home/webservice/api/current/storage -ln -sfn /home/webservice/api/shared/.env /home/webservice/api/current/.env - -# 의존성 설치 + 최적화 -cd /home/webservice/api/current -composer install --no-dev --optimize-autoloader -php artisan config:cache -php artisan route:cache -php artisan view:cache -php artisan migrate --force -``` - -**Nginx 설정 (api.sam.it.kr):** - -```nginx -# /etc/nginx/sites-available/api.sam.it.kr -server { - listen 80; - server_name api.sam.it.kr; - - root /home/webservice/api/current/public; - index index.php; - - access_log /var/log/nginx/api.sam.it.kr.access.log; - error_log /var/log/nginx/api.sam.it.kr.error.log; - - include /etc/nginx/snippets/security.conf; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm-api.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_read_timeout 60; - } - - client_max_body_size 50M; -} -``` - -### ⑦-stage Laravel API Stage (stage-api.sam.it.kr) ✅ (2026-02-24) - -```bash -# 디렉토리 (API와 동일 구조) -sudo mkdir -p /home/webservice/api-stage/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} -sudo chown -R hskwon:webservice /home/webservice/api-stage -sudo chmod -R 2775 /home/webservice/api-stage - -# 배포 절차는 API와 동일, DB는 sam_stage 사용 -``` - -**Nginx 설정 (stage-api.sam.it.kr):** - -```nginx -# /etc/nginx/sites-available/stage-api.sam.it.kr -server { - listen 80; - server_name stage-api.sam.it.kr; - - root /home/webservice/api-stage/current/public; - index index.php; - - access_log /var/log/nginx/stage-api.sam.it.kr.access.log; - error_log /var/log/nginx/stage-api.sam.it.kr.error.log; - - include /etc/nginx/snippets/security.conf; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm-api-stage.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_read_timeout 60; - } - - client_max_body_size 50M; -} -``` - -### ⑧ Laravel Admin 배포 (admin.codebridge-x.com) ✅ (2026-02-24) - -```bash -sudo mkdir -p /home/webservice/mng/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} -sudo chown -R hskwon:webservice /home/webservice/mng -sudo chmod -R 2775 /home/webservice/mng - -# 배포 절차는 API와 동일 (mng/ 저장소 기준) -``` - -**Nginx 설정 (admin.codebridge-x.com):** - -```nginx -# /etc/nginx/sites-available/admin.codebridge-x.com -server { - listen 80; - server_name admin.codebridge-x.com; - - root /home/webservice/mng/current/public; - index index.php; - - access_log /var/log/nginx/admin.codebridge-x.com.access.log; - error_log /var/log/nginx/admin.codebridge-x.com.error.log; - - include /etc/nginx/snippets/security.conf; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm-admin.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_read_timeout 60; - } - - client_max_body_size 50M; -} -``` - -### ⑨ Sales 배포 (sales.codebridge-x.com) ✅ (2026-02-24) - -```bash -# 디렉토리 (레거시이므로 단순 구조) -sudo mkdir -p /home/webservice/sales -sudo chown -R hskwon:webservice /home/webservice/sales - -# 코드 배포 -cd /home/webservice -git clone sales - -# .env 설정 -cp /home/webservice/sales/.env.example /home/webservice/sales/.env -# .env: DB_HOST=localhost, DB_NAME=codebridge, DB_USER=codebridge, DB_PASS=<비밀번호> -chmod 600 /home/webservice/sales/.env -chmod 775 /home/webservice/sales/uploads -``` - -> **참고**: sales는 순수 PHP 앱. session.php → .env 로드 → lib/mydb.php DB 연결. - -**Nginx 설정 (sales.codebridge-x.com):** - -```nginx -# /etc/nginx/sites-available/sales.codebridge-x.com -server { - listen 80; - server_name sales.codebridge-x.com; - - root /home/webservice/sales; - index index.php index.html; - - access_log /var/log/nginx/sales.codebridge-x.com.access.log; - error_log /var/log/nginx/sales.codebridge-x.com.error.log; - - include /etc/nginx/snippets/security.conf; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm-sales.sock; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_read_timeout 60; - } - - # uploads PHP 실행 차단 (보안) - location ~* /uploads/.*\.php$ { - deny all; - } - - client_max_body_size 50M; -} -``` - -### ⑩ 랜딩페이지 (codebridge-x.com) ✅ (2026-02-24) - -```bash -sudo mkdir -p /home/webservice/landing -sudo chown -R hskwon:webservice /home/webservice/landing - -# 간단한 index.html 배치 -``` - -**Nginx 설정 (codebridge-x.com):** - -```nginx -# /etc/nginx/sites-available/codebridge-x.com -server { - listen 80; - server_name codebridge-x.com www.codebridge-x.com; - - root /home/webservice/landing; - index index.html; - - access_log /var/log/nginx/codebridge-x.com.access.log; - error_log /var/log/nginx/codebridge-x.com.error.log; - - location / { - try_files $uri $uri/ /index.html; - } -} -``` - -### ⑪ Node.js 22.17.1 + Next.js 15.5.12 + PM2 6.0.14 ✅ (2026-02-24) - -```bash -# Node.js 22 LTS -curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - -sudo apt install -y nodejs - -# PM2 설치 -sudo npm install -g pm2 - -# 운영 디렉토리 -sudo mkdir -p /home/webservice/react/{releases,shared} -sudo chown -R hskwon:webservice /home/webservice/react - -# Stage 디렉토리 -sudo mkdir -p /home/webservice/react-stage/{releases,shared} -sudo chown -R hskwon:webservice /home/webservice/react-stage - -# 초기 배포 (개발서버에서 빌드된 소스 tar pipe 전송) -ssh sam-dev "cd /home/webservice && tar cf - --exclude='node_modules' react/" | \ - ssh sam-prod "cd /home/webservice && tar xf -" -cd /home/webservice/react/current -npm install # 의존성 설치 (빌드 결과물은 이미 포함) - -# ⚠️ 빌드는 CI/CD 서버(Jenkins)에서 수행 → 운영서버로 rsync -# 운영서버에서 직접 npm run build 하지 않음 -``` - -**PM2 설정:** - -```javascript -// /home/webservice/ecosystem.config.js -module.exports = { - apps: [ - { - // 운영: cluster 모드 (무중단 배포 지원) - name: 'sam-front', - cwd: '/home/webservice/react/current', - script: 'node_modules/next/dist/bin/next', - args: 'start -p 3000', - instances: 2, - exec_mode: 'cluster', - max_memory_restart: '300M', - env: { - NODE_ENV: 'production', - NODE_OPTIONS: '--max-old-space-size=256' - } - }, - { - // Stage: fork 모드 (리소스 절약) - name: 'sam-front-stage', - cwd: '/home/webservice/react-stage/current', - script: 'node_modules/next/dist/bin/next', - args: 'start -p 3100', - instances: 1, - exec_mode: 'fork', - max_memory_restart: '200M', - env: { - NODE_ENV: 'production', - NODE_OPTIONS: '--max-old-space-size=128' - } - } - ] -}; -``` - -```bash -# PM2 시작 + 부팅 시 자동시작 -cd /home/webservice -pm2 start ecosystem.config.js -pm2 save -pm2 startup - -# 무중단 배포 (운영) -pm2 reload sam-front # cluster 모드에서 zero-downtime reload -``` - -**Nginx 설정 (sam.it.kr):** - -```nginx -# /etc/nginx/sites-available/sam.it.kr -upstream nextjs_prod { - server 127.0.0.1:3000; - keepalive 32; -} - -server { - listen 80; - server_name sam.it.kr; - - access_log /var/log/nginx/sam.it.kr.access.log; - error_log /var/log/nginx/sam.it.kr.error.log; - - location /_next/static/ { - alias /home/webservice/react/current/.next/static/; - expires 365d; - add_header Cache-Control "public, immutable"; - } - - location / { - proxy_pass http://nextjs_prod; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } -} -``` - -**Nginx 설정 (stage.sam.it.kr):** - -```nginx -# /etc/nginx/sites-available/stage.sam.it.kr -upstream nextjs_stage { - server 127.0.0.1:3100; -} - -server { - listen 80; - server_name stage.sam.it.kr; - - access_log /var/log/nginx/stage.sam.it.kr.access.log; - error_log /var/log/nginx/stage.sam.it.kr.error.log; - - location / { - proxy_pass http://nextjs_stage; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } -} -``` - -### ⑫ SSL 인증서 (Let's Encrypt) ✅ (2026-02-24) - -```bash -# Nginx 사이트 활성화 -sudo ln -s /etc/nginx/sites-available/sam.it.kr /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/api.sam.it.kr /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/admin.codebridge-x.com /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/sales.codebridge-x.com /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/codebridge-x.com /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/stage.sam.it.kr /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/stage-api.sam.it.kr /etc/nginx/sites-enabled/ -sudo nginx -t && sudo systemctl reload nginx - -# SSL 인증서 발급 (전체 develop@codebridge-x.com) -sudo certbot --nginx -d sam.it.kr --email develop@codebridge-x.com -sudo certbot --nginx -d api.sam.it.kr --email develop@codebridge-x.com -sudo certbot --nginx -d stage.sam.it.kr --email develop@codebridge-x.com -sudo certbot --nginx -d stage-api.sam.it.kr --email develop@codebridge-x.com -sudo certbot --nginx -d admin.codebridge-x.com --email develop@codebridge-x.com -sudo certbot --nginx -d sales.codebridge-x.com --email develop@codebridge-x.com -sudo certbot --nginx -d codebridge-x.com -d www.codebridge-x.com --email develop@codebridge-x.com - -# 자동 갱신 확인 -sudo certbot renew --dry-run # 7/7 success -sudo systemctl status certbot.timer -``` - -**인증서 현황 (만료일: 2026-05-24, 알림: develop@codebridge-x.com):** - -| 도메인 | 상태 | -|--------|------| -| sam.it.kr | ✅ | -| api.sam.it.kr | ✅ | -| stage.sam.it.kr | ✅ | -| stage-api.sam.it.kr | ✅ | -| admin.codebridge-x.com | ✅ | -| sales.codebridge-x.com | ✅ | -| codebridge-x.com + www | ✅ | - -**인증서 현황 (만료일: 2026-05-24):** - -| 도메인 | 알림 이메일 | -|--------|------------| -| sam.it.kr | admin@codebridge-x.com | -| api.sam.it.kr | admin@codebridge-x.com | -| stage.sam.it.kr | admin@codebridge-x.com | -| stage-api.sam.it.kr | admin@codebridge-x.com | -| admin.codebridge-x.com | develop@codebridge-x.com | -| sales.codebridge-x.com | develop@codebridge-x.com | -| codebridge-x.com + www | develop@codebridge-x.com | - -### ⑬ 모니터링 에이전트 (node_exporter 1.8.2) ✅ (2026-02-24) - -```bash -# node_exporter 설치 (Prometheus 메트릭 수집) -cd /tmp -wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz -tar xzf node_exporter-1.8.2.linux-amd64.tar.gz -sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/ -rm -rf node_exporter-1.8.2* - -# systemd 서비스 등록 -sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF' -[Unit] -Description=Node Exporter -After=network.target - -[Service] -User=nobody -ExecStart=/usr/local/bin/node_exporter -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - -sudo systemctl daemon-reload -sudo systemctl enable node_exporter -sudo systemctl start node_exporter - -# 확인: http://localhost:9100/metrics -curl -s localhost:9100/metrics | head -5 -``` - -> CI/CD 서버의 Prometheus가 `211.117.60.189:9100`을 스크래핑. -> UFW에서 CI/CD IP만 9100 포트 허용. - -### ⑭ 최종 점검 ✅ (2026-02-24) - -> 커널 리부트 완료 (6.8.0-41 → 6.8.0-100), 전체 서비스 자동 복구 확인 - -```bash -# 서비스 상태 - sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter -pm2 status - -# 방화벽 -sudo ufw status verbose - -# 메모리 -free -h - -# 디스크 -df -h - -# 포트 -sudo ss -tlnp - -# Redis -redis-cli ping - -# SSL 인증서 -sudo certbot certificates -``` - ---- - -## 4. 디렉토리 구조 - -``` -/home/webservice/ -├── api/ # Laravel API (운영) -│ ├── current → releases/20260223/ -│ ├── releases/ -│ │ └── 20260223/ -│ └── shared/ -│ ├── .env -│ └── storage/ -│ ├── app/public/ -│ ├── framework/{cache,sessions,views}/ -│ └── logs/ -│ -├── api-stage/ # Laravel API (Stage) -│ ├── current → releases/... -│ ├── releases/ -│ └── shared/ (.env, storage/) -│ -├── mng/ # Laravel Admin -│ ├── current → releases/... -│ ├── releases/ -│ └── shared/ (.env, storage/) -│ -├── sales/ # Plain PHP (레거시) -│ ├── .env -│ ├── index.php -│ ├── lib/ -│ ├── uploads/ -│ └── ... -│ -├── react/ # Next.js (운영) -│ ├── current → releases/... -│ ├── releases/ -│ └── shared/ (.env.local) -│ -├── react-stage/ # Next.js (Stage) -│ ├── current → releases/... -│ ├── releases/ -│ └── shared/ (.env.local) -│ -├── landing/ # 정적 랜딩페이지 -│ └── index.html -│ -└── ecosystem.config.js # PM2 설정 (운영 + Stage) -``` - ---- - -## 5. DB 마이그레이션 ✅ (2026-02-24) - -### SAM 메인 DB (sam) - -```bash -# 개발서버에서 덤프 -ssh sam-dev "mysqldump -u codebridge -p sam > /tmp/sam_dump.sql" -scp sam-dev:/tmp/sam_dump.sql /tmp/ - -# 운영서버로 전송 + 임포트 -scp /tmp/sam_dump.sql sam-prod:/tmp/ -ssh sam-prod "sudo mysql sam < /tmp/sam_dump.sql" -ssh sam-prod "rm /tmp/sam_dump.sql" -``` - -### 통계 DB (sam_stat) - -```bash -ssh sam-dev "mysqldump -u codebridge -p sam_stat > /tmp/sam_stat_dump.sql" -scp sam-dev:/tmp/sam_stat_dump.sql /tmp/ -scp /tmp/sam_stat_dump.sql sam-prod:/tmp/ -ssh sam-prod "sudo mysql sam_stat < /tmp/sam_stat_dump.sql" -ssh sam-prod "rm /tmp/sam_stat_dump.sql" -``` - -> **참고**: chandj DB는 운영서버로 이관하지 않음. Sales는 codebridge DB 사용. - ---- - -## 6. 개발서버 현황 (참고) - -| 항목 | 개발서버 | 운영서버 | -|------|----------|---------| -| OS | Ubuntu 24.04.2 | Ubuntu 24.04 (kernel 6.8.0-100) | -| CPU/RAM | 2C / 3.8GB (스왑 없음) | 2C / 8GB + 스왑 4GB | -| PHP | 8.4.15 (+ 5.6, 7.3) | 8.4.18 | -| MySQL | **8.0.45** | **8.4.8** | -| Node.js | 22.17.1 | 22.17.1 | -| Nginx | 1.24.0 | 1.24.0 | -| Redis | - | 7.0.15 (로컬, 512mb) | -| Composer | - | 2.9.5 | -| PHP-FPM | 단일 www pool | 4개 분리 (api/admin/sales/stage) | -| PM2 | fork ×1 (:3001) | 6.0.14 cluster ×2 (:3000) | -| Supervisor | - | queue worker ×2 (redis) | -| UFW | **비활성** | 활성 (22/80/443/9100/3306) | -| fail2ban | - | ✅ | -| 디렉토리 | /home/webservice/ | /home/webservice/ | - ---- - -## 7. Claude Code SSH 원격 셋팅 - -### 완료 항목 ✅ - -- [x] SSH 키 인증 (sam-prod, sam-cicd, sam-dev) -- [x] sudo NOPASSWD (3개 서버 모두) -- [x] ~/.ssh/config 등록 (sam-prod, sam-cicd, sam-dev) -- [x] root SSH 차단 + 비밀번호 로그인 차단 (sam-prod, sam-cicd) - -### Claude Code 실행 방식 - -```bash -# 단일 명령 -ssh sam-prod "sudo apt update" - -# 파일 전송 -scp local_file sam-prod:/remote/path/ - -# 설정 파일 생성 (tee + heredoc) -ssh sam-prod "sudo tee /etc/nginx/sites-available/sam.it.kr > /dev/null << 'EOF' -...설정 내용... -EOF" -``` - -### 제약사항 -- 인터랙티브 명령 불가 (vim, mysql_secure_installation 등) -- 명령 타임아웃: 최대 10분 -- `DEBIAN_FRONTEND=noninteractive` 사용 - ---- - -## 8. 보안 체크리스트 - -- [x] SSH 키 인증만 허용 (비밀번호 로그인 비활성화) -- [x] root SSH 로그인 비활성화 -- [x] UFW 방화벽 활성화 -- [x] MySQL root 원격 접근 차단 (auth_socket) -- [x] MySQL 앱 사용자 최소 권한 (codebridge) -- [x] .env 파일 권한 600 (api, admin, sales 모두) -- [x] storage/ 디렉토리 권한 775 -- [x] Nginx security.conf 스니펫 적용 -- [x] PHP display_errors = Off (모든 pool) -- [x] Laravel APP_DEBUG=false, APP_ENV=production -- [x] Sales uploads/ PHP 실행 차단 -- [x] Certbot 자동 갱신 확인 (7/7 dry-run success) -- [x] fail2ban 설치 (SSH 브루트포스 방지) -- [x] Redis bind 127.0.0.1 (외부 접근 차단) -- [x] node_exporter CI/CD IP만 허용 (UFW) - ---- - -## 9. 모니터링 아키텍처 - -``` -운영서버 CI/CD서버 -┌─────────────────┐ ┌────────────────────┐ -│ node_exporter │──(:9100)──→ │ Prometheus │ -│ (CPU/RAM/Disk) │ │ (수집 + 알림) │ -└─────────────────┘ └────────┬───────────┘ - │ - ┌────────▼───────────┐ - │ Grafana │ - │ (대시보드 시각화) │ - └────────────────────┘ -``` - -| 서버 | 프로그램 | 역할 | 메모리 | -|------|----------|------|--------| -| 운영 | node_exporter | 메트릭 수집 (CPU/RAM/디스크/네트워크) | ~10MB | -| CI/CD | Prometheus | 메트릭 저장 + 알림 규칙 | ~200MB | -| CI/CD | Grafana | 대시보드 시각화 | ~100MB | - ---- - -## 10. 향후 계획 - -- [ ] CI/CD 서버 셋팅 (Jenkins + Gitea + Prometheus + Grafana) -- [ ] 자동 배포 파이프라인 (Jenkins → 운영서버) -- [ ] Stage React 배포 (react-stage, PM2 fork :3100) -- [ ] API CORS/Sanctum 설정 (운영 도메인 반영) -- [ ] DB 복제 (Master → Slave) -- [ ] 개발서버 MySQL 8.0 → 8.4 업그레이드 (운영 안정화 후) -- [ ] Docker 기반 전환 검토 -- [ ] 백업 자동화 (DB 일일 백업 → CI/CD 서버) -- [ ] React → Vercel 이전 검토 \ No newline at end of file