diff --git a/deploys/ops-manual/10-backup-recovery.md b/deploys/ops-manual/10-backup-recovery.md index 59dfbf8..cd47e4b 100644 --- a/deploys/ops-manual/10-backup-recovery.md +++ b/deploys/ops-manual/10-backup-recovery.md @@ -265,6 +265,8 @@ gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebrid 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; ``` @@ -273,11 +275,139 @@ UFW에서 CI/CD IP의 MySQL 접근이 허용되어 있어야 합니다: ```bash # 운영 서버 UFW 규칙 확인 sudo ufw status | grep 3306 -# → 110.10.147.46 ALLOW (CI/CD 백업용) +# → 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와 **자동 동기화는 없으며**, 필요 시 수동으로 동기화합니다. @@ -322,48 +452,6 @@ cd /home/webservice/api-stage/current && php artisan config:cache && php artisan --- -## [개발→운영] DB 동기화 - -개발서버(sam-dev)의 sam DB를 운영서버(sam-prod)의 sam DB로 복원하는 절차입니다. - -> ⚠️ **운영 데이터가 덮어쓰기됩니다.** 반드시 운영 백업 후 진행하세요. - -### 절차 - -```bash -# 1. 운영 DB 백업 (안전용, 운영 서버) -ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ - mysqldump -ucodebridge -p\$DB_PASS --no-tablespaces --skip-triggers --skip-routines sam | gzip > /home/webservice/backups/sam_prod_before_sync.sql.gz" - -# 2. 개발 DB 덤프 (개발 서버) -ssh sam-dev "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/.env | head -1 | cut -d= -f2) && \ - mysqldump -ucodebridge -p\$DB_PASS --no-tablespaces --skip-triggers --skip-routines sam | gzip > /tmp/sam_dev.sql.gz" - -# 3. 로컬 경유 전송 (dev→local→prod) -scp sam-dev:/tmp/sam_dev.sql.gz /tmp/sam_dev.sql.gz -scp /tmp/sam_dev.sql.gz sam-prod:/tmp/sam_dev.sql.gz - -# 4. 운영 DB 복원 -ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ - gunzip -c /tmp/sam_dev.sql.gz | mysql -ucodebridge -p\$DB_PASS sam" - -# 5. 검증 -ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ - mysql -ucodebridge -p\$DB_PASS -e \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='sam';\"" - -# 6. 임시 파일 정리 -ssh sam-dev "rm -f /tmp/sam_dev.sql.gz" -ssh sam-prod "rm -f /tmp/sam_dev.sql.gz" -rm -f /tmp/sam_dev.sql.gz -``` - -### 주의사항 -- 개발↔운영 서버 간 직접 SCP 불가 → 로컬 경유 전송 -- `codebridge` 유저 권한 제한으로 `--no-tablespaces --skip-triggers --skip-routines` 필수 -- 롤백: `/home/webservice/backups/sam_prod_before_sync.sql.gz` 사용 - ---- - ## 전체 서버 복구 절차 ### [운영] 복구 순서 @@ -385,15 +473,16 @@ rm -f /tmp/sam_dev.sql.gz 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. 전체 서비스 동작 검증 +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) @@ -526,6 +615,10 @@ for t in data['data']['activeTargets']: # 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 ``` **자동 시작 확인:**