mirim-app/deploy/BACKUP.md
AWESOMEDEV 1b559f5da2
All checks were successful
CI / backend-test (push) Successful in 1m10s
CI / frontend-build (push) Successful in 40s
CI / backend-dep-scan (push) Successful in 28s
fix: 전면 결함 스윕 — 4관점 감사 확정 결함 일괄 수정
4개 전문 감사관(프론트·백엔드·일관성·UX) 병렬 검증 결과 반영.

버그/깨짐(HIGH·MED):
- HtmlCssPage: 존재하지 않는 /learn/computer → /learn/hardware(깨진 링크, 대시보드로 튕김)
- MentorAnalyticsService: 체크리스트 활동(Progress.checkedAt)이 '마지막 활동'에 누락돼
  체크리스트만 한 학생이 '관심 필요'로 오분류되던 버그 수정(+회귀 테스트)
- 퀴즈 핫스팟 평균 정수절삭 → Math.round로 통일

접근성/UX(MED):
- CodeEditor: 코드 입력창에 aria-label 추가(WCAG 4.1.2, 스크린리더 이름 부여)
- 멘토 제출물 표: tr role=button → 셀 안 button으로(표 의미구조 보존 + 키보드)
- 대시보드 체크박스 토글 실패 시 무피드백 → try/catch + 에러 안내

콘텐츠/문서 드리프트:
- 초급 미니프로젝트 JS 예제 var→let(let/const 가르치며 var 쓰던 자기모순)
- README 강좌 수(71→83+48), BACKUP.md 복원검증 카운트(퀴즈655·문서19·코딩25)
- 브랜드/코스수 주석 정리(수습 플랫폼→학습 플랫폼, 72개→비수치화), 퀴즈 결과 '점'→'%'

테스트 백엔드 51→52, 프론트 30 유지. 운영 배포·스모크 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:01:05 +09:00

7.0 KiB
Raw Blame History

백업·복원 운영 가이드

학습 플랫폼 **DB(PostgreSQL)**와 **Git 서버(Gitea)**가 매일 새벽 자동 백업되어 S3에 올라갑니다.

구성

EC2 (3.36.160.246)
├── mirim-postgres 컨테이너   ← 원본 DB
├── mirim-gitea 컨테이너      ← 원본 Git 서버 (저장소 + SQLite + 설정)
├── ~/backups/                ← 서버 로컬 사본 (7일 보관)
└── systemd timer
      ├── 03:00 KST → backup-db.sh     : pg_dump | gzip | s3 cp
      └── 03:10 KST → backup-gitea.sh  : gitea dump | s3 cp   (10분 뒤 — 겹치지 않게)
              ↓
S3: awesomedev-mirim-backup-apne2/
    ├── postgres/*.sql.gz   ← 30일 보관 후 자동 삭제
    └── gitea/*.zip         ← 30일 보관 후 자동 삭제

3-2-1 원칙: 3벌(원본·로컬·S3) / 2종 매체(EC2 디스크·S3) / 1벌 원격(S3)

왜 이렇게 만들었나

결정 이유
RDS 대신 컨테이너 + 백업 학생 5명 규모에 RDS는 월 $15~25가 계속 나감. 백업만 있으면 실질 위험은 같이 사라진다. 게다가 이 compose 파일이 "도커 입문" 코스의 교재다
액세스 키 대신 IAM 역할 키를 파일에 적으면 유출 시 끝. 역할(mirim-backup-role)은 이 서버에서만, 이 버킷의 postgres/ 에만 쓸 수 있다 (최소 권한)
cron 대신 systemd timer Amazon Linux 2023에 cron이 없기도 하고, 타이머는 실행 이력이 journalctl에 남고 Persistent=true로 서버가 꺼졌던 시간대 작업도 따라잡는다
SQL 덤프(pg_dump) 파일 복사가 아니라 "다시 만드는 SQL"이라 PG 버전이 달라도 복원되고, 텍스트라 gzip이 잘 먹는다 (8.5MB → 102KB)
Gitea는 볼륨 tar가 아니라 gitea dump Gitea는 저장소 파일 + SQLite DB(이슈·PR·계정) + 설정이 한 몸이다. 돌아가는 중에 tar로 묶으면 "파일은 새 것, DB는 옛 것"처럼 시점이 어긋날 수 있다. gitea dump는 셋을 일관된 시점으로 묶는다. → 앱이 자기 백업 도구를 제공하면 그걸 먼저 찾아본다

자주 쓰는 명령

# 백업 목록 보기 (로컬 + S3)
~/mirim-app/deploy/restore-db.sh

# 지금 즉시 백업
sudo systemctl start mirim-backup.service         # DB
sudo systemctl start mirim-gitea-backup.service   # Gitea

# 백업 이력·로그 확인
sudo journalctl -u mirim-backup.service -n 30
sudo journalctl -u mirim-gitea-backup.service -n 30

# 다음 실행 예정 시각 (둘 다)
systemctl list-timers 'mirim-*'

# DB 복원 (확인 프롬프트 있음. 복원 전 현재 상태를 자동으로 먼저 백업함)
~/mirim-app/deploy/restore-db.sh mirim-2026-07-17_030000.sql.gz
docker compose -f docker-compose.prod.yml restart backend   # 복원 후 재시작

Gitea 복원 방법

Gitea 복원은 자주 쓸 일이 아니고 단계가 많아 스크립트 대신 절차로 둔다. (자동화하지 않는 것도 선택이다 — 드물게 쓰는 위험한 작업은 사람이 한 단계씩 확인하는 편이 안전하다.)

# 1) 백업 받아서 풀기
aws s3 cp s3://awesomedev-mirim-backup-apne2/gitea/gitea-2026-07-17_031000.zip /tmp/
cd /tmp && unzip -q gitea-2026-07-17_031000.zip -d gitea-restore

# 2) Gitea 중지
cd ~/mirim-app/deploy && docker compose -f docker-compose.prod.yml stop gitea

# 3) 저장소·설정·DB 되돌리기 (볼륨에 직접 씀 — sudo 필요)
V=/var/lib/docker/volumes/deploy_gitea-data/_data
sudo cp -a /tmp/gitea-restore/repos/.        $V/git/repositories/
sudo cp -a /tmp/gitea-restore/data/.         $V/gitea/
sudo cp    /tmp/gitea-restore/app.ini        $V/gitea/conf/app.ini
# gitea-db.sql → SQLite로 되돌리기
sudo rm -f $V/gitea/gitea.db
docker run --rm -v deploy_gitea-data:/data -v /tmp/gitea-restore:/r alpine/sqlite \
  sh -c "sqlite3 /data/gitea/gitea.db < /r/gitea-db.sql"
sudo chown -R 1000:1000 $V

# 4) 다시 시작
docker compose -f docker-compose.prod.yml start gitea

저장소만 급히 살리면 되는 경우엔 훨씬 간단하다 — zip 안의 repos/awesomedev/mirim-app.git은 그 자체로 완전한 bare 저장소라, git clone repos/awesomedev/mirim-app.git 하면 바로 소스가 나온다. 실제로 이 방법으로 복원 검증을 했다 (아래 참고).

복원 리허설 (권장: 분기 1회)

복원해 본 적 없는 백업은 백업이 아니다.

운영 DB를 건드리지 않고 임시 DB에 복원해 검증하는 방법:

F=$(ls -t ~/backups/mirim-*.sql.gz | head -1)
docker exec mirim-postgres psql -U mirim -d postgres -c 'CREATE DATABASE restore_test;'
gunzip -c $F | docker exec -i mirim-postgres psql -U mirim -d restore_test
# 건수 대조
docker exec mirim-postgres psql -U mirim -d restore_test -c \
  "SELECT count(*) FROM users; SELECT count(*) FROM quiz_question;"
docker exec mirim-postgres psql -U mirim -d postgres -c 'DROP DATABASE restore_test;'

Gitea도 같은 방식으로 검증할 수 있다 (백업 속 저장소에서 직접 clone):

F=$(ls -t ~/backups/gitea-*.zip | head -1)
mkdir -p ~/gtest && cd ~/gtest && unzip -qo $F 'repos/*'
docker run --rm --entrypoint sh -v ~/gtest:/w -w /w alpine/git:latest -c '
  git config --global --add safe.directory "*"
  git clone -q repos/awesomedev/mirim-app.git restored && cd restored
  git log --oneline -3; git rev-list --count HEAD'
sudo rm -rf ~/gtest

최초 검증 결과 (2026-07-17)

대상 결과
PostgreSQL 임시 DB에 복원 → 운영 원본과 완전 일치 (퀴즈 655 / 문서 19 / 체크리스트 46 / 코딩 25)
Gitea 백업 속 저장소에서 clone 성공 → HEAD·커밋 20개·파일 269개가 운영과 완전 일치

무엇이 백업되고, 무엇이 안 되나

대상 백업 비고
학생 진도·제출물·퀴즈 성적·코딩 제출 postgres 이게 진짜 자산 — 코드에 없는 유일한 데이터
플랫폼 사용자 계정(비밀번호 해시) postgres
코스·문서·퀴즈 문항·체크리스트 postgres (없어도 시드로 재생성 가능)
Git 저장소(소스·전체 이력) gitea 개발자 PC에도 clone 사본이 있어 이중 안전
Gitea 이슈·PR 리뷰 코멘트 gitea Gitea에만 있는 것 — 잃으면 복구 불가
Gitea 계정·권한·설정 gitea
EC2 서버 설정 자체 재구축은 deploy/ + DEPLOY 문서로 가능

백업 설계의 출발점은 **"무엇이 이 서버에만 있는가"**를 묻는 것이다. 소스 코드는 git의 분산 구조 덕에 개발자 PC마다 사본이 있지만, 이슈·PR 코멘트·학생 제출물은 이 서버에만 있다 — 그래서 이 둘을 지킨다.

비용

항목 크기 비용
postgres 백업 102KB × 30일 ≈ 3MB ~$0
gitea 백업 2.8MB × 30일 ≈ 84MB ~$0.002/월
업로드 전송 무료
합계 ~87MB 월 몇 원 (≈$0)