สำรองข้อมูลสมองของเพื่อน AI แบบเข้ารหัสไว้นอกสถานที่ด้วย AWS S3 + restic — ย้ายความทรงจำออกไปโดยไม่อัปโหลดแบบ plaintext
บทนำ
ผมดำเนินการระบบส่วนตัวที่ทำหน้าที่เหมือนความทรงจำระยะยาวภายนอก โดยใส่บันทึกงานประจำวันและบันทึกการตัดสินใจต่างๆ ลงไป แล้วมันจะดึงสิ่งที่มีความหมายใกล้เคียงที่สุดออกมาให้ด้วยการค้นหา ระบบนี้เรียกว่า RAG (กลไกที่เรียกคืนบันทึกในอดีตผ่านการค้นหาแบบ vector) ซึ่งปรัชญาการออกแบบและเหตุการณ์ที่ข้อมูลพังไปสองครั้งระหว่างใช้งาน ผมเขียนไว้ในบทความแยกต่างหากแล้ว รายละเอียดพื้นฐานขอฝากไว้ที่บทความเหล่านั้น
คืนที่ AI แก้ไขฉัน 5 ครั้ง - ปรัชญาการออกแบบเพื่อให้ RAG ส่วนตัวเป็นเพื่อนคุณเป็นเวลา 5 ปี
ตัดสินใจว่าจะ “สำรองข้อมูล” แต่ไม่เคยทำจริง จนทำลาย Vector DB ความจำไปสองครั้ง
ในตอนท้ายของบทความเรื่องแบ็กอัพนั้น ผมจบไว้เพียงว่า “จะออกแบบระบบแบ็กอัพเข้ารหัสระยะไกล” บทความนี้คือบันทึกต่อเนื่องที่ผมลงมือสร้างการออกแบบนั้นขึ้นจริง รันมันจริง และตรวจสอบไปจนถึงขั้นตอนการกู้คืนจริง สิ่งที่ใช้มีเพียงสองอย่างคือ AWS S3 (บริการจัดเก็บออบเจกต์ของ Amazon) และ restic (เครื่องมือแบ็กอัพเฉพาะทาง) ขอสรุปประเด็นสำคัญไว้ก่อนดังนี้
- ย้ายข้อมูลออกไปนอกสถานที่โดยไม่ทำลายหลักการ “ไม่อัปโหลด plaintext (ข้อมูลดิบที่ยังไม่เข้ารหัส) ขึ้นคลาวด์”
- ไม่ทำให้เป็นแบ็กอัพครั้งเดียวจบ แต่ต่อยอดเข้ากับ daily batch ที่ทำงานอยู่แล้ว ให้ทำงานได้แบบไร้คนดูแล
- ไม่จบแค่ “วางไว้แล้ว” แต่จะถือว่าเสร็จสมบูรณ์ก็ต่อเมื่อยืนยันได้ว่าสามารถกู้คืนกลับมาที่อื่นได้จริง
ทำไมการอพยพข้อมูลไว้ในเครื่อง (local) เพียงอย่างเดียวจึงยังไม่พอ
ความทรงจำนี้เคยพังมาแล้วสองครั้งในอดีต ทั้งครั้งแรกและครั้งที่สอง สาเหตุมาจากการที่หลายโพรเซสเข้าไปแตะ DB รูปแบบที่เปิดไฟล์โดยตรงพร้อมกัน รายละเอียดทั้งหมดอยู่ในบทความแยกต่างหาก
ทุกครั้งที่พัง ผมกู้คืนจากแบ็กอัพที่เก็บไว้ในเครื่อง แต่ความจริงที่ว่ามันมีอยู่แค่ในเครื่องเท่านั้น เป็นสิ่งที่ค้างคาใจมาตลอด การอพยพข้อมูลไว้ในเครื่อง (local) ปกป้องเราได้จาก “พังเพราะทำผิดพลาดเอง” แต่ถ้าตัวเครื่อง PC เองเสีย ถูกขโมย หรือเจอภัยพิบัติ ข้อมูลที่อพยพไว้ก็จะหายไปพร้อมกันด้วย ความทรงจำที่ผมนำออกมาไว้ภายนอกจึงถูกผูกติดอยู่กับอายุขัยของเครื่องเพียงเครื่องเดียว นี่คือขีดจำกัดของการอพยพข้อมูลในเครื่องเพียงอย่างเดียว
ดังนั้นผมจึงอยากเก็บไว้ในที่ห่างไกล (remote) แต่ตรงนี้เองที่มีข้อกำหนดสองข้อซึ่งไม่อาจอยู่ร่วมกันได้โดยตรงมาปะทะกัน
จะทำให้ “ไม่อัปโหลด plaintext” กับ “เก็บไว้นอกสถานที่” อยู่ร่วมกันได้อย่างไร
เนื้อหาของบันทึกคือบทสนทนาส่วนตัวและประวัติการตัดสินใจต่างๆ ผมไม่อยากอัปโหลดมันขึ้นคลาวด์แบบดิบๆ ในทางกลับกัน การจะทนต่อภัยพิบัติได้ก็ต้องเก็บไว้ในที่ที่ต่างจากที่เก็บในเครื่อง นั่นคือนอกสถานที่ (off-site) เท่านั้น “ไม่อยากอัปโหลด” กับ “ถ้าไม่อัปโหลดก็ปกป้องไม่ได้” ดูเหมือนจะขัดแย้งกันเองในแวบแรก
สิ่งที่ไขปัญหานี้ได้คือการเข้ารหัสฝั่งไคลเอนต์ (client-side encryption) ที่ restic มีมาให้เป็นค่าเริ่มต้น การเข้ารหัสฝั่งไคลเอนต์คือการ “เข้ารหัสข้อมูลในเครื่องก่อนที่จะส่งออกไป” restic จะแปลงข้อมูลให้กลายเป็นก้อนที่เข้ารหัสด้วย AES-256 (ความแข็งแรงของการเข้ารหัสที่ใช้กันอย่างแพร่หลายในปัจจุบัน) ก่อนส่งไปยัง S3 กุญแจถอดรหัสมีอยู่แค่ในเครื่องของผมเอง และสำเนาสำรองที่ผมเก็บไว้แบบออฟไลน์เท่านั้น
data/ (plaintext = ต้นฉบับหลักของบทสนทนาและบทเรียน)
│
▼
┌──────────────────────────────┐
│ restic เข้ารหัสในเครื่อง │ AES-256 / กุญแจอยู่ในเครื่องเท่านั้น
└─────────────┬────────────────┘
│ ก้อนข้อมูลที่เข้ารหัสแล้ว (อ่านเนื้อหาไม่ได้อีกต่อไป)
┌────────┴───────────────────┐
▼ ▼
สำเนาในเครื่อง ส่งไปยัง AWS S3
│ │
│ ▼
│ ผู้ที่ไม่มีกุญแจ
│ (รวมถึงผู้ให้บริการ S3 เอง)
└──────────────────▶ ไม่มีวันอ่านเนื้อหาได้
เนื่องจากสิ่งที่วางอยู่บน S3 มีเพียงก้อนที่เข้ารหัสแล้วเท่านั้น หลักการ “ไม่อัปโหลด plaintext” จึงไม่ถูกทำลาย แม้แต่ผู้ให้บริการคลาวด์เองก็มองไม่เห็นเนื้อหาข้างใน สิ่งที่ดูเหมือนความขัดแย้งก็หายไป เมื่อผมกำหนดตำแหน่งของการเข้ารหัสไว้ที่ “ก่อนส่ง”
ที่เลือก restic เป็นวิธีการนั้น เพราะมันเป็นไฟล์ปฏิบัติการเดี่ยวที่รันได้บน Windows โดยตรง มี incremental (ส่งเฉพาะส่วนต่างจากครั้งก่อน) และการเข้ารหัสมาให้เป็นค่ามาตรฐาน มีเครื่องมือยอดนิยมที่คล้ายกันอย่าง borg แต่การรองรับ Windows ยังอ่อนอยู่ จึงตัดออกไป
ไม่พึ่งพาวิธีเดียว — เก็บไว้สามชั้น
ไม่ใช่แค่เพิ่มสำเนานอกสถานที่อีกหนึ่งชุด แต่ผมออกแบบให้เป็นโครงสร้างที่ “ต่อให้ชุดใดชุดหนึ่งตายไป ความทรงจำก็ยังคงอยู่” ขอบเขตของสิ่งที่ต้องปกป้องจำกัดอยู่ที่ data/ (ไฟล์ Markdown และ DB ที่เป็นต้นฉบับหลักของบันทึก ขนาดประมาณ 16MB) ส่วนดัชนีสำหรับการค้นหาแบบ vector เป็นสิ่งที่สร้างขึ้นใหม่ได้จากต้นฉบับหลักนี้ จึงไม่จำเป็นต้องขนไปไว้ไกลถึงนอกสถานที่
| ชั้น | เป้าหมาย | ที่เก็บ | ลักษณะ |
|---|---|---|---|
| ① | ตัว vector DB เอง | ในเครื่อง (เก็บรุ่นย้อนหลังไว้) | สแนปช็อตก่อนการทำ operation ที่ทำลายข้อมูล |
| ② | ต้นฉบับหลัก data/ | ที่เก็บ restic ในเครื่อง | ไม่ต้องพึ่ง AWS ทำงานได้เสมอ |
| ③ | ต้นฉบับหลัก data/ | restic บน S3 (เข้ารหัส) | นอกสถานที่อย่างแท้จริง |
data/ จะถูกเก็บไว้สามชั้นคือ ②+③+(ประวัติ git เช่นเดียวกับโค้ด) ทั้ง ② และ ③ จะลบรุ่นเก่าทิ้งโดยอัตโนมัติ เหลือไว้เพียงประมาณ 30 รุ่นล่าสุด การไม่พึ่งพาบริการเดียวอย่าง S3 ทั้งหมด — นี่คือการออกแบบที่ตั้งใจ การมีสำเนานอกสถานที่มักทำให้เราชะล่าใจและมองข้ามสำเนาในเครื่อง แต่ผมทำในทางตรงกันข้าม สำเนาในเครื่อง (②) จะทำงานเสมอไม่ว่าสถานะของ AWS จะเป็นอย่างไร ส่วน S3 (③) วางตำแหน่งไว้เป็นเพียงประกันเพิ่มเติมที่ซ้อนทับอยู่บนนั้น
ผลพลอยได้ — ต่อยอดเข้ากับ daily batch ที่มีอยู่แล้วโดยตรง
แบ็กอัพนี้ผมไม่ได้ทำเป็นสคริปต์เดี่ยวๆ แต่นำไปต่อท้ายเป็นขั้นตอนหลังของ daily batch ที่รันอยู่ทุกวันเวลา 04:00 อยู่แล้ว (กระบวนการที่เรียกว่า distill ซึ่งยกระดับโน้ตให้กลายเป็นบทเรียน รายละเอียดอยู่ในบทความแยกต่างหาก)
ตรงนี้ผมอยากทำให้ชัดเจนในจุดที่ผู้อ่านสับสนได้ง่ายที่สุด batch นี้ไม่ได้ทำงานอยู่บน GitHub มันทำงานอยู่บน PC ของผมเอง โค้ดถูกวางไว้บน GitHub ก็จริง แต่สิ่งที่ GitHub ทำให้ก็แค่ “เก็บรักษา data/ ที่ push เข้าไป” เท่านั้น ไม่ได้รันการประมวลผลให้ตอน 04:00 ผู้ที่ทำหน้าที่เข้ารหัสและส่งไปยัง S3 ตามเวลาที่กำหนดคือ Windows Task Scheduler ในเครื่องของผมเองเท่านั้น
┌── พลาดรอบ 04:00 ──┐ ← PC ปิดอยู่/หลับอยู่
│ │
Windows Task ─────┴─ ตามคืนตอนล็อกอินครั้งถัดไป ┘ (catch-up)
(ทุกวัน 04:00 บน PC ในเครื่อง)
│
▼
(1) อพยพ vector DB ──ล้มเหลว──▶ ยกเลิกขั้นตอนที่เหลือ (ไม่มีฐาน ก็ไปต่อไม่ได้)
│ สำเร็จ
▼
(2) distill (ยกระดับโน้ต → บทเรียน)
│
├── ล้มเหลว ───────────────┐ ← ไม่ร้ายแรงถึงชีวิต: ไม่ทำให้ batch ทั้งหมดล่ม
▼ │
(3) แบ็กอัพหลายชั้นของ data/ ◀───┘
├─▶ ② restic ในเครื่อง (ไม่ต้องพึ่ง AWS ทำงานได้เสมอ)
└─▶ ③ S3 restic ─┬─ มี auth profile ─▶ ส่งข้อมูล
│ └─ ไม่มี / หมดอายุ ────▶ ไม่ข้ามแบบเงียบๆ บันทึกว่า "ข้าม"
▼
(4) กระบวนการถอนบทเรียนที่ผิด (ไปยังบทความคู่กัน)
จุดสำคัญของการออกแบบนี้อยู่ที่ edge ย้อนกลับของ “ถ้าพลาดก็ตามคืนตอนล็อกอินครั้งถัดไป (catch-up)” และการแยกกิ่งภายในขั้นตอนส่งไป S3 ผมไม่ได้ออกแบบให้ต้องปลุกเครื่องให้ทำงานตามเวลาที่กำหนดเสมอ เหตุผลจะเขียนแยกไว้ในสองหัวข้อถัดไป
เพื่อให้ทำงานแบบไร้คนดูแล — ทำไมจึงเลือกใช้ long-lived key แบบสิทธิ์ขั้นต่ำสุด แทนที่จะเป็น SSO
การส่งข้อมูลไปยัง S3 ต้องยืนยันตัวตนกับ AWS ก่อน สำหรับงานที่ทำด้วยมือตามปกติ SSO (วิธีที่ล็อกอินครั้งเดียวแล้วใช้งานได้ระยะเวลาหนึ่ง) ก็เพียงพอแล้ว แต่ batch นี้ทำงานแบบไร้คนดูแลตอน 04:00 โทเค็นของ SSO (บัตรผ่านชั่วคราว) จะหมดอายุตามเวลา จึงอาจหมดอายุไปแล้วในช่วงที่ batch ทำงานแบบไร้คนดูแล
สิ่งที่ยุ่งยากคือวิธีที่มันพังเมื่อหมดอายุ การยืนยันตัวตนหมดอายุจนส่งไม่ได้ แต่ batch ก็จบลงเหมือนไม่มีอะไรเกิดขึ้น ไม่มี error ขึ้นใน log ด้วยซ้ำ — ความล้มเหลวเงียบๆ ที่ดูเหมือนความสำเร็จ (silent false negative) สำหรับแบ็กอัพแล้วนี่คือสิ่งที่เลวร้ายที่สุด มันก่อให้เกิดสถานการณ์ “คิดว่าแบ็กอัพได้อยู่ แต่จริงๆ แล้วไม่ได้มาหลายเดือนแล้ว”
ผมจึงสร้างผู้ใช้เฉพาะขึ้นมาหนึ่งรายบนฝั่ง AWS ที่อ่านเขียนได้เฉพาะ bucket (ที่เก็บข้อมูล) นี้เท่านั้น และวาง long-lived key ที่ไม่หมดอายุไว้ใน profile ในเครื่อง สิทธิ์ถูกจำกัดไว้เฉพาะ operation ที่จำเป็นเท่านั้น (least privilege) นโยบายของ IAM (การจัดการสิทธิ์ของ AWS) มีรูปแบบคร่าวๆ ดังนี้
// อนุญาตเฉพาะ list / get / put / delete บน bucket นี้เท่านั้น (ไม่อนุญาตอย่างอื่นเลย)
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::mint-chroma-backup-<アカウントID>",
"arn:aws:s3:::mint-chroma-backup-<アカウントID>/*"
]
}
ต่อให้กุญแจนี้รั่วไหลออกไป สิ่งที่ทำได้ก็มีแค่อ่านเขียน bucket สำหรับแบ็กอัพนี้เท่านั้น ไม่สามารถแตะต้อง resource อื่นใดของ AWS ได้เลย นี่คือทางเลือกที่ตอบโจทย์ทั้ง “ไม่ล้มเหลวแบบเงียบๆ” และ “จำกัดความเสียหายแม้จะรั่วไหล” ไปพร้อมกัน ถ้า auth profile ยังไม่ได้ตั้งค่าหรือหมดอายุ ③ จะถูกบันทึกว่า “ข้าม” โดยไม่ข้ามแบบเงียบๆ ตามที่แสดงในแผนภาพข้างต้น ส่วน ② จะไม่หยุดทำงาน
ยอมรับความล้มเหลว แล้วเอาคืนด้วยการ recovery — ทางเลือกที่ไม่ปลุกเครื่องตามเวลา
การตัดสินใจออกแบบอีกอย่างหนึ่งคือสวิตช์สองตัวใน Task Scheduler
- ถ้าพลาดก็ตามคืนตอนเริ่มเครื่องครั้งถัดไป (เปิดใช้ catch-up)
- ไม่ปลุก PC ตามเวลาที่กำหนด (ไม่ auto-start ตอนตี 4)
กล่าวคือ “ถ้า PC หลับอยู่ตอน 04:00 ก็ยอมแพ้รอบนั้นไปตามตรง แต่จะตามคืนตอนที่ล็อกอินครั้งถัดไป” ผมเลือกนโยบายที่ยอมรับความล้มเหลวได้ แต่ต้อง recovery เสมอ แทนที่จะยึดติดกับการรันตามเวลาที่กำหนดจนต้องปลุกเครื่องกลางดึก สำหรับการใช้งานส่วนตัว ผมตัดสินใจว่าความเสียหายจริง (ไฟฟ้า อายุการใช้งาน เสียงรบกวน) จากการปลุกเครื่องหนึ่งเครื่องทุกคืนตอนตี 4 ไม่คุ้มกับความเร่งด่วนของแบ็กอัพรายวัน ผมตรวจสอบว่ามันทำงานอย่างไรจริงๆ ตอนที่เครื่องหลับ โดยปล่อยให้มันหลับแล้วดู log ในเช้าวันถัดไป
repository does not exist — ตรวจสอบความจริงก่อนกระโดดไปสู่สมมติฐานที่ซับซ้อน
อาจฟังดูเหมือนเป็นการออกแบบที่เรียบร้อยมาถึงตรงนี้ แต่ตอนที่ผมประกอบฝั่ง S3 เป็นครั้งแรก restic ไม่ยอมผ่านเอาเสียเลย สิ่งที่ได้กลับมามีแค่ repository does not exist (ที่เก็บแบ็กอัพไม่มีอยู่) ซ้ำแล้วซ้ำเล่า
ตอนแรกผมสงสัยปัญหาเรื่อง authentication จากนั้นสงสัยวิธีเขียน endpoint (รูปแบบที่อยู่ของ S3) แล้วยังสงสัยไปถึงความแตกต่างในการจัดการ file path อีกด้วย สมมติฐานยิ่งขยายไปในทางที่ซับซ้อนขึ้นเรื่อยๆ ผมหยุดมือแล้วกลับไปที่การตรวจสอบพื้นฐานที่สุด — ที่เก็บข้อมูลนั้นมีอยู่จริงหรือเปล่า
# ก่อนกระโดดไปสู่สมมติฐานที่ซับซ้อน ให้ดูก่อนว่า "มีอยู่จริงหรือไม่" ในครั้งเดียว
aws s3 ls s3://mint-chroma-backup-<アカウントID>/ --recursive
ว่างเปล่า ไม่ใช่ปัญหาเรื่อง authentication หรือ endpoint เลย เพียงแค่ยังไม่ได้ทำการ initialize (restic init) เพื่อสร้างที่เก็บแบ็กอัพเท่านั้นเอง พอ initialize ใหม่อย่างสะอาด มันก็ผ่านไปได้อย่างง่ายดาย บทเรียนชัดเจนคือ เมื่อเห็น repository does not exist ให้ตรวจสอบว่ามันมีอยู่จริงหรือไม่สักครั้งก่อน ก่อนที่จะกระโดดไปสู่สมมติฐานที่ซับซ้อนอย่าง authentication หรือรูปแบบ path
อีกเรื่องหนึ่งที่ผมตัดสินใจในงานนี้คือ กระบวนการที่พึ่งพากุญแจในเครื่องอย่าง restic จะไม่ผสม shell กัน การสลับไปมาระหว่าง PowerShell กับ shell อื่นภายในขั้นตอนเดียวกัน มักนำไปสู่อุบัติเหตุที่สร้างที่เก็บ passphrase ว่างเปล่าขึ้นมา เนื่องจากความไม่ตรงกันของ environment ผมจึงกำหนดตายตัวในขั้นตอนว่าจะรวมเป็น PowerShell เดียว
เมื่อรันด้วยมือเพียงครั้งเดียว การเรียกใช้จริงจะมีรูปแบบดังนี้ (ค่ากุญแจและบางส่วนของที่อยู่ถูกปิดบังไว้)
# อ่านกุญแจจากไฟล์ที่อยู่นอกการจัดการของ git (ไม่ทิ้ง plaintext ไว้ใน repository)
$env:RESTIC_PASSWORD = (Get-Content "<鍵ファイル>")[0]
$env:RESTIC_REPOSITORY = "s3:s3.ap-northeast-1.amazonaws.com/mint-chroma-backup-<アカウントID>"
restic backup "<data ディレクトリ>" --tag chroma-s3
restic snapshots # ตรวจสอบรายการ snapshot ที่แบ็กอัพได้
ไม่จบแค่ “วางไว้แล้ว” — เสร็จสมบูรณ์ก็ต่อเมื่อกู้คืนได้จริง
ผมไม่ได้นิยามความสำเร็จของแบ็กอัพว่า “ส่งแล้ว” แต่จะถือว่าเสร็จสมบูรณ์ก็ต่อเมื่อได้กู้คืนกลับมาไว้ในไดเรกทอรีว่างอีกอันหนึ่งจริงๆ แล้วนำจำนวนไฟล์และจำนวนไบต์มาเทียบกับต้นฉบับแล้วตรงกัน บทเรียนในอดีตที่เคยพังไปสองครั้งเพราะแค่ “ตัดสินใจว่าจะแบ็กอัพ” โดยไม่เคยสร้างสิ่งที่เป็นรูปธรรมขึ้นจริง คือแกนหลักของนิยามนี้
ในการทดสอบกู้คืนครั้งนี้ สิ่งที่กู้คืนมาจาก S3 คือ 235 ไฟล์ 16,154,894 ไบต์ ตรงกับต้นฉบับโดยไม่มีความต่างแม้แต่ไบต์เดียว ตัวเลขนี้เองไม่ได้มีความหมายอะไร สิ่งที่มีความหมายคือข้อเท็จจริงที่ว่า “กลับมาโดยไม่ขาดหายแม้แต่ไบต์เดียว” ซึ่งเป็นหลักฐานเพียงหนึ่งเดียวที่ยืนยันว่า “แบ็กอัพนี้เป็นของจริง และในยามฉุกเฉินสามารถเรียกความทรงจำกลับคืนมาได้จริง”
นี่คือผลลัพธ์จริงของการตรวจสอบการกู้คืน (ปกปิดชื่อบัคเก็ตและ repo ID) บรรทัดนี้ยืนยันว่าไม่ใช่แค่ “ส่งไปแล้ว” แต่ “กู้กลับมาได้จริง”
$ restic snapshots
ID Time Tags Paths
--------------------------------------------------
xxxxxxxx 2026-07-28 04:01:58 chroma-s3 .../data
$ restic restore latest --target <empty-dir>
restored 235 files / 16,154,894 bytes
ขอเรียงลำดับนิยามของความสำเร็จไว้ดังนี้
- initialize ที่เก็บข้อมูล (
restic init) - ยืนยันว่ากุญแจใช้งานได้ (ถอดรหัสการตั้งค่าเข้ารหัสแล้วอ่านได้หรือไม่)
- ส่งแบ็กอัพ
- กู้คืนกลับมาไว้ในที่อื่นจริงๆ แล้วตรวจสอบจำนวนไฟล์และจำนวนไบต์
- ตั้ง budget alert (หัวข้อถัดไป)
ไม่ใช่คำประกาศ แต่มีเพียง log การวัดผลจริงในข้อ 4 เท่านั้นที่เป็นหลักฐานของความสำเร็จ
เก็บได้เกือบฟรี — แต่ต้องกำจัดกับดักต้นทุนคงที่ก่อน
ค่าใช้จ่ายที่น่าเป็นห่วงอยู่ที่ต่ำกว่า $0.01 ต่อเดือน — แทบจะเป็นศูนย์ ด้วยกลไก incremental และ deduplication (ไม่เก็บเนื้อหาเดียวกันซ้ำสองครั้ง) ของ restic ที่ทำงานได้ผล ข้อมูลที่เข้ารหัสแล้วซึ่งวางอยู่บน S3 จึงมีขนาดเพียงไม่กี่ MB หลังบีบอัด การจัดเก็บออบเจกต์คิดค่าใช้จ่ายตามการใช้งานจริง (“จำนวน GB ที่วาง” บวก “จำนวนครั้งที่เข้าถึง”) ไม่มีต้นทุนคงที่แบบเซิร์ฟเวอร์ที่เปิดทำงานตลอดเวลา
แต่ AWS นี่เองที่เคยทำให้ผมเจ็บตัวมาแล้วกับ “ต้นทุนคงที่” นี้แหละ ถ้าปล่อยให้องค์ประกอบที่เปิดทำงานตลอดเวลาหลงเหลืออยู่โดยไม่ตั้งใจ ค่าใช้จ่ายก็จะเกิดขึ้นต่อเนื่องแม้จะไม่ได้ใช้งานอยู่ก็ตาม รายละเอียดเรื่องนี้อยู่ในบทความแยกต่างหาก
ค้นพบต้นทุนคงที่ของ ECS Fargate (ALB + NAT Gateway) และการทบทวนสถาปัตยกรรม
การ Deploy ไปผิด Region ด้วย AWS CDK + PowerShell + SSO และการแก้ไขปัญหา
ดังนั้นครั้งนี้ แม้จะรู้อยู่แล้วว่าราคาถูก ผมก็ยังติดตั้งวาล์วนิรภัยไว้ก่อน ผมตั้ง budget alert ไว้ให้ส่งอีเมลเตือนเมื่อถึง 80% ของ $1 ต่อเดือน (≈ $0.80) เมื่อเทียบกับที่คาดไว้ว่าจะต่ำกว่า $0.01 แล้ว หากมันมาถึงจุดนั้นจริงๆ แสดงว่าต่างกันไปเป็นหลักสิบเท่าแล้ว สิ่งนี้ไม่ได้อยู่ที่จำนวนเงินเอง แต่เป็นกลไกที่จะจับสถานการณ์ที่ “ค่าใช้จ่ายที่ต่างจากที่คาดไว้เป็นหลักอย่างเงียบๆ กำลังเติบโตขึ้น” ให้ได้ก่อนที่จะแตะเพดานสูงสุด
ตัดความทรงจำออกจากอายุขัยของเครื่องเดียวได้สำเร็จ
ด้วยเหตุนี้ ความทรงจำที่ผมนำออกมาไว้ภายนอกจึงไม่ถูกผูกติดกับความเป็นความตายของ PC เครื่องเดียวในมืออีกต่อไป แม้จะพังเพราะทำผิดพลาดเอง ก็กู้คืนจากสำเนาในเครื่องได้ แม้จะสูญเสีย PC เครื่องนั้นไปทั้งเครื่อง ก้อนข้อมูลที่เข้ารหัสไว้ก็ยังคงเหลืออยู่ในที่ห่างไกล และก้อนข้อมูลนั้นก็ไม่มีใครที่ไม่มีกุญแจอ่านได้เลย ผมไม่ต้องยอมแพ้ทั้ง “ไม่อัปโหลด plaintext” และ “ทนต่อภัยพิบัติ” เลยแม้แต่ข้อเดียว
สิ่งที่เตรียมไว้จนถึงตรงนี้คือด้าน “ไม่พัง” ความทรงจำยังต้องการการเตรียมพร้อมอีกด้านหนึ่งคือ “ไม่ปนเปื้อน” — ต่อให้เก็บมาได้อย่างถูกต้อง แต่ถ้าเนื้อหามีบทเรียนที่ผิดปะปนอยู่ เพื่อน AI ก็จะนึกถึงมันทุกครั้งและตัดสินใจผิดพลาด ส่วนวิธีถอนความปนเปื้อนนั้นโดยอัตโนมัติ ผมเขียนไว้ในบทความคู่กันถัดไปนี้
และกลไกตรวจสอบประจำสัปดาห์ที่รักษาทั้งหมดนี้ให้สมบูรณ์ ผมเขียนไว้ในบทความนี้