คู่มือการสำรองและกู้คืนข้อมูล (Backup & Restore Guide)
อ้างอิง TOR: ข้อ 4.11.2 (Backup Guide ในชุดเอกสารถ่ายโอนระบบ) และภาคผนวก ก ข้อ 1.6.3 (คู่มือ Backup/Restore + ทดสอบ Restore อย่างน้อย 1 ครั้ง + รายงาน)
ฉบับ: 1.0 — วันที่ 3 ตุลาคม พ.ศ. 2569
สคริปต์ที่อ้างถึง: scripts/_db-common.sh, scripts/backup-db.sh, scripts/restore-db.sh, scripts/restore-test.sh
รายงานผลทดสอบ: /documents/restore-test-report (สร้างอัตโนมัติด้วย npm run report:restore-test)
หัวข้อ 2–6 ใช้กับระบบสาธิต (SQLite + โฟลเดอร์ไฟล์อัปโหลด) ซึ่งทดสอบแล้ว หัวข้อ 7 เป็นแนวทางสำหรับ Production (PostgreSQL 16 + Object Storage) ซึ่ง ยังไม่ได้ดำเนินการจริง เพราะยังไม่ได้ติดตั้งบนโครงสร้างพื้นฐานของมหาวิทยาลัย
1. ข้อมูลที่ต้องสำรอง
| ข้อมูล | ตำแหน่ง (ระบบสาธิต) | สำรองโดย | หมายเหตุ |
|---|---|---|---|
| ฐานข้อมูลทั้งหมด (ผู้ใช้ บทสนทนา คลังความรู้ config prompt ผลประเมิน Audit Log ฯลฯ) | prisma/dev.db (หรือไฟล์ตาม DATABASE_URL/DB_PATH) | backup-db.sh | ใช้ sqlite3 .backup ปลอดภัยแม้แอปกำลังเขียน (โหมด WAL) |
| ไฟล์อัปโหลดต้นฉบับของฐานองค์ความรู้ | storage/uploads/ (STORAGE_DIR) | backup-db.sh (tar) | จำเป็นต่อการ re-index |
| ไฟล์สำเนาข้อมูลส่วนบุคคล (คำขอ PDPA) | โฟลเดอร์ข้าง STORAGE_DIR (storage/privacy-exports) | ไม่รวมในสคริปต์ | สร้างใหม่ได้จากคำขอ; สำรองเพิ่มเองถ้าต้องการ |
| คีย์เข้ารหัส | DATA_ENCRYPTION_KEY, AUTH_SECRET ใน .env | ต้องเก็บแยกอย่างปลอดภัย | หาก DATA_ENCRYPTION_KEY หาย ข้อมูลอีเมล/ชื่อที่เข้ารหัสในไฟล์สำรองจะถอดรหัสไม่ได้ |
| ซอร์สโค้ด/config/prompt ต้นฉบับ/seed | Git repository | ระบบควบคุมเวอร์ชัน | — |
| Log | logs/app.log | ระบบ log ของเครื่อง/คลัสเตอร์ | ไม่จำเป็นต่อการกู้คืน |
2. ความต้องการ
bash(รองรับ bash 3.2 บน macOS และ bash 5 บน Debian),sqlite3(ถ้าไม่มีสคริปต์ออกด้วยรหัส 127 พร้อมคำแนะนำติดตั้ง),tar, เครื่องมือ SHA-256 (sha256sum/shasum/openssl)- รันจากโฟลเดอร์ใดก็ได้ สคริปต์ย้ายไปที่รากโปรเจกต์เอง
2.1 การหาไฟล์ฐานข้อมูล (_db-common.sh → resolve_db_path)
ลำดับความสำคัญ:
DB_PATH(สัมพัทธ์กับรากโปรเจกต์)DATABASE_URLใน environmentDATABASE_URLในไฟล์.env- ค่าเริ่มต้น
prisma/dev.db
DATABASE_URL แบบ file:./dev.db ตีความสัมพัทธ์กับโฟลเดอร์ prisma/ (เหมือน Prisma) และตัด query string ออก ถ้า DATABASE_URL ไม่ใช่ file: (เช่น PostgreSQL) สคริปต์ปฏิเสธพร้อมแนะนำให้ใช้ pg_dump/PITR ตามหัวข้อ 7
3. การสำรองข้อมูล (scripts/backup-db.sh)
bash scripts/backup-db.sh
# ตัวอย่างระบุปลายทาง/ฐานข้อมูล
BACKUP_DIR=/mnt/backup/mbu DATABASE_URL=file:./prod.db bash scripts/backup-db.sh
ขั้นตอนที่สคริปต์ทำ:
- หาไฟล์ฐานข้อมูล (หัวข้อ 2.1) — ไม่พบไฟล์ = หยุดพร้อมข้อความแนะนำ
- ตั้งชื่อไฟล์ตามเวลา
YYYYMMDD-HHMMSS(ถ้าซ้ำในวินาทีเดียวกันต่อท้าย-1,-2, …) sqlite3 <db> ".backup '<BACKUP_DIR>/<ts>.db'"- ตรวจ
PRAGMA integrity_checkของไฟล์สำรอง — ไม่ใช่okจะลบไฟล์สำรองและออกด้วยข้อผิดพลาด; ลบไฟล์-wal/-shmว่างที่ค้าง ให้เหลือไฟล์.dbไฟล์เดียว - ถ้ามีโฟลเดอร์ไฟล์อัปโหลด สร้าง
<ts>-storage.tar(ตั้งCOPYFILE_DISABLE=1กัน macOS ใส่ไฟล์._*) - พิมพ์คำสั่งกู้คืน และ บรรทัดสุดท้ายของ stdout เป็น path ของไฟล์
.db(ให้โปรแกรมอื่นอ่านต่อได้ — หน้า/admin/backupใช้ค่านี้)
| ตัวแปร | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
DB_PATH / DATABASE_URL | prisma/dev.db | ฐานข้อมูลต้นทาง |
BACKUP_DIR | backups | โฟลเดอร์ปลายทาง |
STORAGE_DIR | storage/uploads | โฟลเดอร์ไฟล์อัปโหลด |
ผลลัพธ์: backups/<ts>.db และ backups/<ts>-storage.tar (ถ้ามีไฟล์อัปโหลด)
3.1 สำรองผ่านหน้าเว็บ
SUPER_ADMIN ใช้หน้า /admin/backup → สำรองข้อมูลตอนนี้ (API POST /api/admin/backup เรียก scripts/backup-db.sh, timeout 120 วินาที) หน้าเว็บแสดงรายการไฟล์ในโฟลเดอร์สำรองพร้อมคำสั่งกู้คืนของแต่ละไฟล์ — ไม่มีปุ่มกู้คืนในหน้าเว็บเพราะต้องหยุดแอปก่อน
3.2 กำหนดเวลาสำรองอัตโนมัติ (แนะนำ)
# สำรองทุกวันเวลา 02:00 และเก็บ log
0 2 * * * cd /opt/mbu-buddhist-ai && bash scripts/backup-db.sh >> logs/backup.log 2>&1
# ลบไฟล์สำรองที่เก่ากว่า 30 วัน (ปรับตามนโยบายของมหาวิทยาลัย)
30 2 * * * find /opt/mbu-buddhist-ai/backups -maxdepth 1 -type f -mtime +30 -delete
แนวปฏิบัติ: เก็บสำเนาไว้นอกเครื่อง (อีกเครื่อง/Object Storage) อย่างน้อย 1 ชุด, เข้ารหัสไฟล์สำรองที่ส่งออกนอกเครื่อง (ไฟล์มีข้อมูลบทสนทนา), จำกัดสิทธิ์อ่านโฟลเดอร์ backups/
4. การกู้คืนข้อมูล (scripts/restore-db.sh)
# 1) หยุดแอป (npm run dev / npm start / server.https.js)
# 2) กู้คืน
bash scripts/restore-db.sh backups/20261002-182126.db
# 3) เปิดแอปใหม่ แล้วตรวจที่ /api/health และ /admin/dashboard
ขั้นตอนที่สคริปต์ทำ:
- ตรวจอาร์กิวเมนต์ (ไม่มี = รหัสออก 2) และหาไฟล์สำรอง (absolute, สัมพัทธ์กับโฟลเดอร์ที่เรียก หรือสัมพัทธ์กับรากโปรเจกต์)
- ปฏิเสธถ้าไฟล์สำรองเป็นไฟล์เดียวกับฐานข้อมูลปลายทาง
- ตรวจ
PRAGMA integrity_checkของไฟล์สำรองก่อนเขียนทับ — ไม่ผ่าน = ยกเลิก ฐานข้อมูลเดิมไม่ถูกแตะต้อง - คัดลอกทับแบบ atomic (คัดลอกเป็นไฟล์ชั่วคราวในโฟลเดอร์เดียวกันแล้ว
mv) และลบ-wal/-shm/-journalที่ค้าง - ถ้ามี
<ชื่อเดียวกัน>-storage.tarแตกไฟล์กลับเข้าSTORAGE_DIR(เลือกข้าม/ล้างก่อนได้) - ตรวจ
PRAGMA integrity_checkหลังกู้คืนและรายงานจำนวนตาราง
| ตัวแปร | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
DB_PATH / DATABASE_URL | prisma/dev.db | ฐานข้อมูลปลายทาง |
STORAGE_DIR | storage/uploads | โฟลเดอร์ไฟล์อัปโหลดปลายทาง |
RESTORE_STORAGE=0 | (กู้ไฟล์อัปโหลด) | กู้เฉพาะฐานข้อมูล |
RESTORE_STORAGE_CLEAN=1 | (ไม่ล้าง) | ล้างโฟลเดอร์อัปโหลดก่อนแตกไฟล์ ให้ตรงกับ ณ เวลาสำรองทุกประการ |
รหัสออก: 0 สำเร็จ, 1 ผิดพลาด/ไม่ผ่าน integrity_check, 2 ใช้คำสั่งไม่ถูกต้อง
4.1 หลังกู้คืน
- เปิดแอปใหม่ — ดัชนีค้นหาในหน่วยความจำสร้างใหม่จากฐานข้อมูลที่กู้คืนเมื่อมีคำขอแรก
- ตรวจ
GET /api/health(จำนวน chunk ต้องไม่เป็น 0 ถ้ามีคลังความรู้) - ที่
/admin/audit-logกด ตรวจสอบความสมบูรณ์ (hash chain) - ถ้าไฟล์อัปโหลดกู้คืนไม่ครบ ใช้ Re-index ที่
/admin/knowledge-baseเพื่อตรวจว่าทุกเอกสารอ่านไฟล์ต้นฉบับได้ (เอกสารที่ไฟล์หายจะล้มเหลวพร้อมข้อความ) - ข้อมูลที่เกิดหลังเวลาสำรองจะหายไป (RPO = ช่วงห่างของการสำรอง) — แจ้งผู้ใช้ตามแผนสื่อสาร
5. การทดสอบการกู้คืน (scripts/restore-test.sh)
npm run report:restore-test # = bash scripts/restore-test.sh
KEEP_RESTORE_TEST_FILES=1 bash scripts/restore-test.sh # เก็บไฟล์ทดสอบไว้ตรวจย้อนหลัง
ความปลอดภัย: สคริปต์ไม่แตะฐานข้อมูลจริงและ storage/uploads จริง — อ่านฐานข้อมูลจริงครั้งเดียวเพื่อทำสำเนา (sqlite3 .backup) ไปที่ prisma/restore-test.db และคัดลอกไฟล์อัปโหลดไปโฟลเดอร์ชั่วคราวใต้ backups/.restore-test-* แล้วลบทั้งหมดเมื่อจบ
ขั้นตอน (ทุกขั้นบันทึกผล ok/fail/skip ลงรายงาน):
- ทำสำเนาฐานข้อมูลและไฟล์อัปโหลดเพื่อทดสอบ
- สำรองด้วย
scripts/backup-db.sh - บันทึกจำนวนแถวทุกตาราง + SHA-256 ของเนื้อหาทั้งฐานข้อมูล (
.dump) - เพิ่มตาราง marker
_restore_test_marker(ข้อมูลที่เกิดหลังการสำรอง) แล้ว ลบไฟล์ฐานข้อมูลและไฟล์อัปโหลด (จำลองความเสียหาย) - กู้คืนด้วย
scripts/restore-db.sh(วัดเวลา = RTO ที่วัดได้) - ตรวจ: จำนวนแถวรายตารางตรงกับก่อนสำรอง,
PRAGMA integrity_check= ok, ตาราง marker ต้องหายไป, SHA-256 ของ.dumpตรงกัน, จำนวนไฟล์อัปโหลดตรงกัน - เขียน
docs/deliverables/restore-test-report.md(frontmatter ของศูนย์เอกสาร, สรุปผล, ตารางรายตาราง, สภาพแวดล้อม)
รหัสออก 0 = ผ่าน, 1 = ไม่ผ่าน (รายงานถูกเขียนทั้งสองกรณี) ตัวแปรเสริม: RESTORE_TEST_DB, BACKUP_DIR, STORAGE_DIR, REPORT_PATH, KEEP_RESTORE_TEST_FILES
ผลการทดสอบล่าสุด ดูที่ /documents/restore-test-report — แนะนำให้ทดสอบซ้ำอย่างน้อยทุกไตรมาส และทุกครั้งที่เปลี่ยน schema หรือโครงสร้างพื้นฐาน
6. ขั้นตอนกู้คืนเหตุฉุกเฉิน (Runbook)
| สถานการณ์ | ขั้นตอน |
|---|---|
| ไฟล์ฐานข้อมูลเสีย/ลบโดยไม่ตั้งใจ | หยุดแอป → เลือกไฟล์สำรองล่าสุดที่ผ่าน integrity → restore-db.sh → เปิดแอป → ตรวจตามหัวข้อ 4.1 |
| ข้อมูลผิดจากการตั้งค่า AI/prompt | ไม่ต้องกู้ฐานข้อมูล — ย้อนกลับ snapshot ที่ /admin/ai-config หรือเวอร์ชัน prompt ที่ /admin/prompts |
| ไฟล์อัปโหลดหายแต่ฐานข้อมูลปกติ | RESTORE_STORAGE_CLEAN=1 แล้วแตกเฉพาะ tar ด้วย tar -xf backups/<ts>-storage.tar -C storage/uploads หรือกู้คืนพร้อม DB แล้ว Re-index |
| เครื่องเสียทั้งเครื่อง | ติดตั้งระบบใหม่ตาม /documents/deployment-guide (ไม่ต้อง seed) → ใส่ .env เดิม (คีย์เข้ารหัสเดิม) → คัดลอกไฟล์สำรองจากที่เก็บนอกเครื่อง → restore-db.sh |
| อัปเกรดแล้ว migration ผิดพลาด | กู้จากไฟล์สำรองก่อนอัปเกรด แล้วย้อนแอปเป็นเวอร์ชันเดิม |
7. แนวทางสำหรับ Production (PostgreSQL 16 + Object Storage)
สคริปต์ข้างต้นใช้กับ SQLite เท่านั้น สำหรับ Production ตาม Solution Architecture แนะนำ:
| องค์ประกอบ | วิธีสำรอง | วิธีกู้คืน |
|---|---|---|
| PostgreSQL 16 | สำรองเต็มรายวันด้วย pg_dump -Fc หรือ snapshot ของบริการฐานข้อมูลคลาวด์ + WAL archiving สำหรับ Point-in-Time Recovery (PITR) | pg_restore เข้าฐานข้อมูลใหม่ หรือ PITR ไปยังเวลาก่อนเกิดเหตุ แล้วสลับ DATABASE_URL |
| Object Storage (ไฟล์อัปโหลด) | เปิด versioning + replication ข้าม region/บัญชี | กู้ object version ก่อนหน้า |
| Elasticsearch | snapshot repository หรือสร้างดัชนีใหม่จากฐานข้อมูล/ไฟล์ต้นฉบับ (re-index) | restore snapshot หรือ reindex_all |
| Redis | ไม่จำเป็นต้องกู้ (ข้อมูลชั่วคราว: rate limit, คิว, cache) | — |
Secret (mbu-buddhist-ai-secrets) | เก็บใน secret manager ของคลาวด์ / Sealed Secrets ที่สำรอง key ไว้ | สร้าง Secret ใหม่จาก secret manager |
| Helm release | helm history + Git ของ values | helm rollback / ติดตั้งใหม่จาก Git |
เป้าหมายที่เสนอ (ต้องตกลงกับมหาวิทยาลัย): RPO ≤24 ชั่วโมง (สำรองรายวัน) หรือ ≤15 นาทีเมื่อใช้ PITR, RTO ≤4 ชั่วโมง และทดสอบกู้คืนบน namespace mbu-test อย่างน้อยไตรมาสละครั้ง พร้อมออกรายงานในรูปแบบเดียวกับ /documents/restore-test-report