# 10. 백업, 복구, 재부팅 [목차로 돌아가기](./README.md) --- ## [운영] DB 백업 ### 수동 백업 ```bash # sam DB mysqldump -u hskwon --single-transaction --routines --triggers sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz # sam_stat DB mysqldump -u hskwon --single-transaction --routines --triggers sam_stat | gzip > /tmp/sam_stat_$(date +%Y%m%d_%H%M%S).sql.gz # codebridge DB (Sales) mysqldump -u hskwon --single-transaction --routines --triggers codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz # 전체 DB mysqldump -u hskwon --single-transaction --routines --triggers --all-databases | gzip > /tmp/all_db_$(date +%Y%m%d_%H%M%S).sql.gz ``` ### 파일 백업 (업로드, Storage) ```bash # API storage tar czf /tmp/api_storage_$(date +%Y%m%d).tar.gz -C /home/webservice/api/shared storage # MNG storage tar czf /tmp/mng_storage_$(date +%Y%m%d).tar.gz -C /home/webservice/mng/shared storage # Sales uploads tar czf /tmp/sales_uploads_$(date +%Y%m%d).tar.gz -C /home/webservice/sales uploads # 외부 전송 scp /tmp/*_$(date +%Y%m%d).tar.gz sam-cicd:/home/hskwon/backups/files/ ``` ### .env 백업 ```bash mkdir -p /tmp/env_backup cp /home/webservice/api/shared/.env /tmp/env_backup/api.env cp /home/webservice/mng/shared/.env /tmp/env_backup/mng.env cp /home/webservice/sales/.env /tmp/env_backup/sales.env tar czf /tmp/env_backup_$(date +%Y%m%d).tar.gz -C /tmp env_backup scp /tmp/env_backup_$(date +%Y%m%d).tar.gz sam-cicd:/home/hskwon/backups/env/ rm -rf /tmp/env_backup /tmp/env_backup_*.tar.gz ``` ### DB 복구 ```bash # 전체 DB 복구 gunzip -c /path/to/sam_백업파일.sql.gz | sudo mysql sam # 특정 테이블 sudo mysql sam < /path/to/sam_테이블명_백업파일.sql ``` --- ## [CI/CD] Gitea 백업/복구 ### 백업 ```bash # 전체 백업 (저장소 + DB + 설정) sudo mkdir -p /home/hskwon/backups/gitea sudo -u git /usr/local/bin/gitea dump \ --config /etc/gitea/app.ini \ --tempdir /tmp \ --file /home/hskwon/backups/gitea/gitea-dump-$(date +%Y%m%d).zip # 저장소만 sudo tar czf /home/hskwon/backups/gitea/repos-$(date +%Y%m%d).tar.gz \ /var/lib/gitea/data/repositories/ # DB만 mysqldump --single-transaction gitea | gzip > /home/hskwon/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz ``` ### 복구 ```bash sudo systemctl stop gitea cd /tmp unzip /home/hskwon/backups/gitea/gitea-dump-YYYYMMDD.zip mysql -u root gitea < gitea-db.sql sudo rsync -av gitea-repo/ /var/lib/gitea/data/repositories/ sudo chown -R git:git /var/lib/gitea/data/repositories/ sudo cp app.ini /etc/gitea/app.ini sudo chown git:git /etc/gitea/app.ini sudo systemctl start gitea ``` --- ## [CI/CD] Jenkins 백업/복구 ### 백업 ```bash sudo mkdir -p /home/hskwon/backups/jenkins # Jobs 설정 sudo tar czf /home/hskwon/backups/jenkins/jobs-$(date +%Y%m%d).tar.gz \ -C /var/lib/jenkins jobs/ # Credentials sudo tar czf /home/hskwon/backups/jenkins/secrets-$(date +%Y%m%d).tar.gz \ -C /var/lib/jenkins secrets/ credentials.xml # 플러그인 목록 sudo ls /var/lib/jenkins/plugins/*.jpi 2>/dev/null | xargs -I{} basename {} .jpi \ > /home/hskwon/backups/jenkins/plugins-$(date +%Y%m%d).txt # 환경변수 파일 sudo tar czf /home/hskwon/backups/jenkins/env-files-$(date +%Y%m%d).tar.gz \ -C /var/lib/jenkins env-files/ # SSH 키 sudo tar czf /home/hskwon/backups/jenkins/ssh-$(date +%Y%m%d).tar.gz \ -C /var/lib/jenkins .ssh/ ``` ### 복구 ```bash sudo systemctl stop jenkins sudo tar xzf /home/hskwon/backups/jenkins/jobs-YYYYMMDD.tar.gz -C /var/lib/jenkins/ sudo tar xzf /home/hskwon/backups/jenkins/secrets-YYYYMMDD.tar.gz -C /var/lib/jenkins/ sudo tar xzf /home/hskwon/backups/jenkins/env-files-YYYYMMDD.tar.gz -C /var/lib/jenkins/ sudo tar xzf /home/hskwon/backups/jenkins/ssh-YYYYMMDD.tar.gz -C /var/lib/jenkins/ sudo chown -R jenkins:jenkins /var/lib/jenkins/ sudo systemctl start jenkins ``` --- ## [CI/CD] Prometheus / Grafana 백업 ```bash # Prometheus 설정 (필수) sudo cp /etc/prometheus/prometheus.yml /home/hskwon/backups/prometheus-config-$(date +%Y%m%d).yml # Prometheus 데이터 (선택, 보존 기간 30일) sudo systemctl stop prometheus sudo tar czf /home/hskwon/backups/prometheus-data-$(date +%Y%m%d).tar.gz /var/lib/prometheus/ sudo systemctl start prometheus # Grafana 설정 + 대시보드 sudo mkdir -p /home/hskwon/backups/grafana sudo cp /etc/grafana/grafana.ini /home/hskwon/backups/grafana/grafana.ini-$(date +%Y%m%d) sudo tar czf /home/hskwon/backups/grafana/grafana-data-$(date +%Y%m%d).tar.gz /var/lib/grafana/ ``` --- ## [CI/CD] MySQL 자동 백업 (운영 DB) ### 개요 CI/CD 서버(sam-cicd)에서 운영 서버(sam-prod)의 MySQL DB를 원격으로 백업합니다. | 항목 | 값 | |------|-----| | 스케줄 | **매일 03:00** (crontab) | | 스크립트 | `/home/hskwon/scripts/backup-db.sh` | | 인증 정보 | `/home/hskwon/.sam_backup.cnf` (chmod 600) | | 저장소 | `/home/hskwon/backups/mysql/` | | 보존 기간 | **14일** (자동 삭제) | | 로그 | `/home/hskwon/backups/mysql/backup.log` | ### 백업 대상 | DB | 서버 | 사용자 | 크기 (gzip) | 비고 | |----|------|--------|------------|------| | gitea | localhost | root (auth_socket) | ~50KB | Gitea DB | | sam | 211.117.60.189 (운영) | sam_backup | ~9.3MB | 운영 메인 DB (295 테이블) | | sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB (20 테이블) | ### 백업 스크립트 ```bash # /home/hskwon/scripts/backup-db.sh #!/bin/bash set -e BACKUP_DIR="/home/hskwon/backups/mysql" BACKUP_CNF="/home/hskwon/.sam_backup.cnf" DATE=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=14 mkdir -p $BACKUP_DIR # Gitea DB 백업 (로컬, auth_socket) mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/gitea_$DATE.sql.gz # 운영 DB 원격 백업 (sam_backup 사용자) if [ -f "$BACKUP_CNF" ]; then mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam | gzip > $BACKUP_DIR/sam_production_$DATE.sql.gz mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam_stat | gzip > $BACKUP_DIR/sam_stat_production_$DATE.sql.gz echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat)" >> $BACKUP_DIR/backup.log else echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log fi # 오래된 백업 삭제 find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete ``` ### 인증 설정 ```ini # /home/hskwon/.sam_backup.cnf (chmod 600) [client] user=sam_backup password=<백업용_비밀번호> ``` ### 크론탭 (sam-cicd 서버, hskwon 유저) ```crontab # SAM DB 백업 (매일 새벽 3시) 0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1 ``` ### 수동 실행 및 확인 ```bash # 수동 백업 실행 /home/hskwon/scripts/backup-db.sh # 백업 파일 확인 ls -lht /home/hskwon/backups/mysql/ # 백업 로그 확인 tail -10 /home/hskwon/backups/mysql/backup.log # 크론 스케줄 확인 crontab -l ``` ### 백업 복원 (CI/CD → 운영) ```bash # sam DB 복원 (운영 서버에서 실행) gunzip -c /path/to/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam # sam_stat DB 복원 gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat ``` ### 운영 MySQL 백업 사용자 (운영 서버 설정) ```sql -- 운영 서버(sam-prod)에서 실행 CREATE USER 'sam_backup'@'110.10.147.46' IDENTIFIED BY '<백업용_비밀번호>'; GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam.* TO 'sam_backup'@'110.10.147.46'; GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam_stat.* TO 'sam_backup'@'110.10.147.46'; GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON codebridge.* TO 'sam_backup'@'110.10.147.46'; GRANT REPLICATION SLAVE ON *.* TO 'sam_backup'@'110.10.147.46'; FLUSH PRIVILEGES; ``` UFW에서 CI/CD IP의 MySQL 접근이 허용되어 있어야 합니다: ```bash # 운영 서버 UFW 규칙 확인 sudo ufw status | grep 3306 # → 110.10.147.46 ALLOW (CI/CD 백업/리플리케이션용) ``` --- ## [CI/CD] MySQL 리플리케이션 (운영 → CI/CD) ### 개요 운영 DB의 변경사항을 실시간으로 CI/CD 서버에 동기화합니다. binlog 기반 리플리케이션으로 변경분만 전송되어 네트워크/디스크 부하가 최소화됩니다. | 항목 | 값 | |------|-----| | 방식 | **MySQL Replication** (Source → Replica) | | Source (운영) | 211.117.60.189, server-id=1 | | Replica (CI/CD) | 110.10.147.46, server-id=2 | | 인증 | `sam_backup@110.10.147.46` (REPLICATION SLAVE) | | 대상 DB | **sam**, **sam_stat**, **codebridge** | | 제외 DB | gitea (CI/CD 자체 DB, 리플리케이션 영향 없음) | | 동기화 | 실시간 (Seconds_Behind_Source ≈ 0) | | CI/CD MySQL | **read_only=OFF** (Gitea DB 쓰기 필요, replicate-do-db로 대상 DB 제한) | ### 아키텍처 ``` [운영 서버 211.117.60.189] [CI/CD 서버 110.10.147.46] MySQL (Source, server-id=1) MySQL (Replica, server-id=2) ┌─────────┐ ┌─────────┐ │ sam │ ── binlog ──────────▶ │ sam │ (read-only) │ sam_stat│ ── binlog ──────────▶ │ sam_stat│ (read-only) │codebridge│── binlog ──────────▶ │codebridge│(read-only) └─────────┘ ├─────────┤ │ gitea │ (독립, read-write) └─────────┘ ``` ### CI/CD MySQL 설정 ```ini # /etc/mysql/mysql.conf.d/sam-tuning.cnf (리플리케이션 관련 부분) [mysqld] server-id = 2 relay-log = /var/log/mysql/mysql-relay-bin # read-only = 1 # Gitea DB 쓰기 필요하여 비활성화 (replicate-do-db로 대상 제한) replicate-do-db = sam replicate-do-db = sam_stat replicate-do-db = codebridge ``` ### 리플리케이션 상태 확인 ```bash # CI/CD 서버(sam-cicd)에서 실행 mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|SQL_Running|Behind|Error' # 정상 상태: # Replica_IO_Running: Yes # Replica_SQL_Running: Yes # Seconds_Behind_Source: 0 # Last_IO_Error: (빈 값) # Last_SQL_Error: (빈 값) ``` ### 리플리케이션 장애 복구 #### IO 스레드 중단 시 ```bash # 에러 확인 mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|IO_Error' # 네트워크 문제: 자동 재연결 (Connect_Retry=60, 10회 시도) # 인증 문제: 운영 서버 sam_backup 유저 확인 # 수동 재시작 mysql -u hskwon -p -e "STOP REPLICA IO_THREAD; START REPLICA IO_THREAD;" ``` #### SQL 스레드 에러 시 ```bash # 에러 확인 mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'SQL_Running|SQL_Error' # 특정 에러 건너뛰기 (주의: 데이터 불일치 가능) mysql -u hskwon -p -e "SET GLOBAL SQL_REPLICA_SKIP_COUNTER = 1; START REPLICA;" ``` #### 전체 재구축 (데이터 불일치 심각 시) ```bash # 1. CI/CD 리플리케이션 중지 mysql -u hskwon -p -e "STOP REPLICA;" # 2. 운영에서 새 덤프 생성 ssh sam-prod "mysqldump -u hskwon -p --databases sam sam_stat codebridge \ --source-data=1 --single-transaction --routines --triggers --events \ --set-gtid-purged=OFF | gzip > /tmp/repl_rebuild.sql.gz" # 3. CI/CD로 전송 scp sam-prod:/tmp/repl_rebuild.sql.gz /tmp/ # 4. CI/CD에서 임포트 zcat /tmp/repl_rebuild.sql.gz | mysql -u hskwon -p # 5. 덤프 헤더에서 binlog position 확인 zcat /tmp/repl_rebuild.sql.gz | head -30 | grep "CHANGE" # 6. 리플리케이션 재설정 (position 값은 위 결과로 교체) mysql -u hskwon -p << 'SQL' CHANGE REPLICATION SOURCE TO SOURCE_HOST='211.117.60.189', SOURCE_USER='sam_backup', SOURCE_PASSWORD='<백업용_비밀번호>', SOURCE_LOG_FILE='binlog.XXXXXX', SOURCE_LOG_POS=XXXXXXXXX, GET_SOURCE_PUBLIC_KEY=1; START REPLICA; SQL # 7. 임시 파일 정리 ssh sam-prod "rm -f /tmp/repl_rebuild.sql.gz" rm -f /tmp/repl_rebuild.sql.gz ``` ### 주의사항 - CI/CD MySQL은 `read_only=OFF` (Gitea가 같은 MySQL 사용하여 쓰기 필요) → **CI/CD에서 sam/sam_stat/codebridge DB에 직접 쓰기 금지** (replicate-do-db 필터로 리플리케이션 대상만 제한) - `replicate-do-db` 필터로 gitea DB는 리플리케이션 영향 없음 - 운영 서버 MySQL 8.4는 `caching_sha2_password` 사용 → 리플리케이션 설정 시 `GET_SOURCE_PUBLIC_KEY=1` 필수 - binlog 보존 기간(`binlog_expire_logs_seconds`) 내에 리플리케이션 장애를 복구해야 함, 초과 시 전체 재구축 필요 --- ## [운영] sam → sam_stage 동기화 Stage 환경(stage-api.sam.it.kr)은 `sam_stage` DB를 사용합니다. 운영 `sam` DB와 **자동 동기화는 없으며**, 필요 시 수동으로 동기화합니다. ### 스키마만 동기화 (테이블 구조) 운영 DB에 테이블/컬럼 변경이 있을 때 실행합니다. ```bash # 운영 서버(sam-prod)에서 실행 # 1. sam_stage 초기화 mysql -ucodebridge -p -e "DROP DATABASE IF EXISTS sam_stage; CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 스키마 복사 (구조만, 데이터 없음) mysqldump -ucodebridge -p --single-transaction --no-data --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage # 3. 데이터 복사 (필요시) mysqldump -ucodebridge -p --single-transaction --no-create-info --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage # 4. Laravel 캐시 갱신 cd /home/webservice/api-stage/current php artisan config:cache php artisan cache:clear ``` ### 전체 동기화 (스키마 + 데이터) Stage 환경을 운영과 동일한 상태로 리셋할 때 실행합니다. ```bash # 운영 서버(sam-prod)에서 실행 mysql -ucodebridge -p -e "DROP DATABASE IF EXISTS sam_stage; CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysqldump -ucodebridge -p --single-transaction --no-tablespaces --skip-triggers --skip-routines sam | mysql -ucodebridge -p sam_stage cd /home/webservice/api-stage/current && php artisan config:cache && php artisan cache:clear ``` ### 주의사항 - `codebridge` 사용자에 `SUPER`, `PROCESS` 권한이 없으므로 `--no-tablespaces --skip-triggers --skip-routines` 옵션 필수 - sam_stage의 `.env`는 별도 관리 (`APP_URL=https://stage-api.sam.it.kr`, `APP_ENV=staging`) - Jenkins 파이프라인(api)의 Stage 배포 시 `php artisan migrate --force`로 스키마 자동 반영 --- ## 전체 서버 복구 절차 ### [운영] 복구 순서 1. OS 설치: Ubuntu 24.04 + 기본 패키지 2. 보안 설정: SSH 키, UFW, fail2ban 3. MySQL 복구: MySQL 8.4 설치 -> 백업 파일 복원 -> 사용자 재생성 4. Redis 설치: Redis 7.x + 설정 5. PHP-FPM 설치: PHP 8.4 + 확장 + Pool 설정 복원 6. Nginx 설치: Nginx + 사이트 설정 복원 + SSL 재발급 7. Node.js + PM2 설치: Node.js 22 + PM2 8. 애플리케이션 배포: 각 서비스 코드 + .env 복원 + storage 복원 9. Supervisor 설치: Queue Worker 설정 10. 모니터링: node_exporter 설치 상세: [서버 설치 가이드](./11-server-setup.md) ### [CI/CD] 복구 순서 1. OS 기본 셋팅 (UFW, 스왑, 타임존) 2. MySQL 설치 + Gitea DB 복원 3. **MySQL 리플리케이션 설정** (sam-tuning.cnf 복원, 운영 DB 덤프 임포트, CHANGE REPLICATION SOURCE) 4. Java 설치 5. Gitea 설치 + 설정/저장소 복원 6. Jenkins 설치 + jobs/credentials/env-files/SSH 키 복원 7. Nginx 설치 + 사이트 설정 + SSL 인증서 발급 8. Prometheus + node_exporter 설치 + 설정 복원 9. Grafana 설치 + 대시보드 임포트 10. fail2ban 설치 11. Webhook 연결 확인 12. 전체 서비스 동작 검증 (리플리케이션 상태 포함) 상세: [서버 설치 가이드](./11-server-setup.md) --- ## 서버 재부팅 절차 ### [운영] 재부팅 **재부팅 전 점검:** ```bash # 서비스 상태 기록 sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter pm2 status # 대기 중인 Queue 작업 확인 cd /home/webservice/api/current && php artisan queue:monitor redis:default # 진행 중인 MySQL 쿼리 확인 sudo mysql -e "SHOW PROCESSLIST;" | grep -v Sleep # PM2 상태 저장 pm2 save # 리소스 상태 기록 free -h df -h ``` **재부팅 실행:** `sudo reboot` **재부팅 후 확인 (1~2분 후):** ```bash # 시스템 상태 uptime && free -h && df -h # 서비스 확인 sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter certbot.timer fail2ban pm2 status # 포트 확인 sudo ss -tlnp # 웹 서비스 응답 curl -sI https://sam.it.kr curl -sI https://api.sam.it.kr curl -sI https://mng.codebridge-x.com curl -sI https://sales.codebridge-x.com curl -sI https://stage.sam.it.kr curl -sI https://stage-api.sam.it.kr # Redis / MySQL 연결 redis-cli ping sudo mysql -e "SELECT 1;" # Queue Worker sudo supervisorctl status # 방화벽 sudo ufw status ``` **서비스 자동 시작 실패 시:** ```bash sudo systemctl start nginx sudo systemctl start php8.4-fpm sudo systemctl start mysql sudo systemctl start redis-server sudo systemctl start supervisor sudo systemctl start node_exporter sudo systemctl start fail2ban pm2 resurrect # 저장된 프로세스 복구 # PM2 복구 실패 시 cd /home/webservice && pm2 start ecosystem.config.js && pm2 save ``` **자동 시작 등록 확인:** ```bash sudo systemctl is-enabled nginx php8.4-fpm mysql redis-server supervisor node_exporter fail2ban # 등록 안 된 서비스: sudo systemctl enable 서비스명 # PM2는 pm2 startup + pm2 save로 관리 ``` --- ### [CI/CD] 재부팅 **재부팅 전 점검:** ```bash # Jenkins 실행 중인 빌드 확인 (웹 UI: https://ci.sam.it.kr) # Gitea 진행 중인 push 확인 sudo tail -5 /var/lib/gitea/log/gitea.log # 서비스 상태 기록 sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter > /tmp/pre-reboot-status.txt ``` **재부팅 실행:** `sudo reboot` **재부팅 후 검증:** ```bash # 서비스 상태 sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter # 포트 확인 sudo ss -tlnp | grep -E '(80|443|3000|3100|8080|9090|9100|3306)' # 웹 서비스 응답 curl -sI https://ci.sam.it.kr | head -3 curl -sI https://git.sam.it.kr | head -3 curl -sI https://monitor.sam.it.kr | head -3 # 리소스 확인 free -h && df -h # 모니터링 연결 curl -s http://localhost:9090/api/v1/targets | python3 -c " import json, sys data = json.load(sys.stdin) for t in data['data']['activeTargets']: print(f\"{t['labels'].get('job','?'):15} {t['health']:6} {t['scrapeUrl']}\") " # MySQL 상태 mysql -e "SHOW GLOBAL STATUS LIKE 'Uptime';" # MySQL 리플리케이션 상태 mysql -u hskwon -p -e "SHOW REPLICA STATUS\G" | grep -E 'IO_Running|SQL_Running|Behind|Error' # → IO_Running: Yes, SQL_Running: Yes, Seconds_Behind: 0 ``` **자동 시작 확인:** ```bash for svc in nginx jenkins gitea mysql prometheus grafana-server node_exporter fail2ban; do echo -n "$svc: " systemctl is-enabled $svc 2>/dev/null || echo "NOT FOUND" done # 비활성 서비스: sudo systemctl enable 서비스명 ```