จากหกแอปเหลือสาม: ปลดระวางแบบที่ปลุกกลับมาได้
10 พฤศจิกายน 2569 · ThaiPo
วันหนึ่งในเดือนสิงหาคม เราทำลาย production ของตัวเองทิ้งไปครึ่งหนึ่งโดยตั้งใจ สามแอป พร้อม volume, dedicated IP address, certificate และ DNS record ของมัน ประหยัดได้ราวเดือนละเก้าดอลลาร์ และนั่นไม่ใช่เหตุผล
เหตุผลคือ surface area ThaiPo ถูกสร้างและดูแลโดย developer คนเดียว และมันโตจนมีแอปที่ deploy อยู่หกตัว: web app, serving plane ที่ทำหน้าที่แปล, learning plane ที่ทำงานเบื้องหลังแบบช้าๆ บวกกับ business console, WhatsApp connector และ LINE OpenChat connector สามตัวหลังเป็นโปรดักต์จริงที่ใช้งานได้จริง และก็เป็นของอีกสามอย่างที่พังได้ตอนตีสาม โดยมีตัวหนึ่งที่ต้องคอยป้อนข้าวป้อนน้ำทุกสัปดาห์

สามตัวที่เกินมา มีต้นทุนจริงๆ เป็นอะไร
ไม่ใช่เงิน แต่เป็นสมาธิ
- งานบ้านรายสัปดาห์ OpenChat connector ถือ session ที่ login ค้างไว้ ซึ่งจะตายราวเจ็ดวันหลังการสแกน QR แต่ละครั้ง ไม่ว่าเครื่องจะ healthy แค่ไหน client library refresh token ของมันได้ แต่ไม่เคยเขียน token ที่หมุนใหม่กลับลง storage session จึงมีอายุยืนกว่าการสแกนที่สร้างมันขึ้นมาไม่ได้ ทุกสัปดาห์: หยิบมือถือเครื่องที่กำหนด สแกนโค้ด เช็ก log เราตั้ง reminder ไว้เตือนเรื่องนี้ด้วย reminder แก้อะไรไม่ได้ มันเตือนได้อย่างเดียว
- Failure mode ที่มารยาทดี สแกน QR นั้นด้วยมือถือผิดเครื่อง ทุกอย่างก็ยังสำเร็จ token หน้าตาถูกต้องถูกเขียนลงไป และ health check บอกว่า healthy มีครั้งหนึ่ง crash loop ใช้โควตา restart ของ host จนหมด แล้วทิ้งเครื่องไว้ในสภาพหยุด การตั้ง credential ใหม่รายงานว่าสำเร็จ แต่ไม่ได้ start อะไรขึ้นมาเลย อยู่ราวเก้านาทีอันเงียบสงบ
- Deploy ที่ deploy ไม่ได้ release หนึ่งล้มเหลวเพราะ volume ของ connector อยู่บน host ที่ติดต่อไม่ได้ ตัว connector เองไม่เป็นไร ยังให้บริการอยู่บน image ตัวก่อน วิธีแก้ที่เห็นชัดๆ คือสร้าง volume ใหม่ในที่ที่ healthy ซึ่งเป็นการกระทำเดียวที่จะทำให้ระบบล่มจริง เพราะ volume คือที่อยู่ของ session
- กฎที่ต้องจำเอาเอง connector ตัวนั้นต้องรันเป็นเครื่องเดียวเป๊ะ สองเครื่องแปลว่ามี session เดียวกันสองชุด และทุกคำแปลถูกโพสต์สองรอบ
ไม่มีข้อไหนดราม่าเลย มันเป็นแค่รายการของที่คนคนเดียวต้องแบกไว้ในหัว
ลบ infrastructure เก็บโค้ดไว้
วิธีปกติในการปลดระวาง feature คือลบทิ้งแล้วรู้สึกสะอาดสะอ้าน เราทำแค่ครึ่งเดียว: infrastructure หายไปแล้ว แต่โค้ดทั้งหมดยังอยู่ใน tree ทั้ง connector ทั้ง console กับ API route 45 ตัวของมัน และ fork ราย channel ของตัวแปล การลบเครื่องย้อนกลับได้ในราคาถูก การเขียน WhatsApp integration ขึ้นใหม่จากความทรงจำไม่ใช่
จุดที่ต้องระวังคือ "โค้ดยังอยู่ แต่ปิดไว้" เป็นประโยคที่คนมักพูดก่อนเกิด incident ไม่นาน งานที่น่าสนใจจึงเป็นการทำให้คำว่า ปิด เป็นคุณสมบัติที่ตรวจสอบได้ ในหลายจุดที่เป็นอิสระต่อกัน แทนที่จะเป็นแค่ความหวัง
สวิตช์ที่หนึ่ง: แถวใน database ที่ใหญ่กว่า environment
feature flag ของเราอยู่ใน table ของ Postgres และสับได้จาก admin console ไม่ต้อง redeploy ไม่ต้อง restart การ resolve ก็เรียบง่าย: ใช้แถวใน database ถ้ามี ไม่มีก็ใช้ environment variable สำหรับ connector สองตัวนั้น Go service ทั้งสองตัวถือว่าแถวใน database เป็นฝ่ายชนะ
ลำดับนี้แหละคือประเด็นทั้งหมด runtime secret ของเรากระจายอยู่ตามแอปต่างๆ และเป็นแบบ write-only: ตั้งค่าได้ แต่อ่านกลับไม่ได้ แถวเดียว ใน table เดียว ที่ SELECT ดูได้ คือสวิตช์ที่ audit ได้ WhatsApp ปิดอยู่ไหม อ่านแถวนั้นดู มันบอกว่า false
สวิตช์ที่สอง: ทำให้ประตูขาเข้าจับคู่ไม่ติด
business console เปิดให้บริษัทเชื่อมต่อบัญชี LINE ของตัวเอง และบัญชีที่เชื่อมไว้แต่ละบัญชีจะถูกค้นด้วย routing key ตอนขาเข้า เราต้องการให้บัญชีพวกนั้นส่ง webhook มาถึงเราไม่ได้แม้แต่ตัวเดียว LINE ไม่มี API สำหรับ toggle "use webhook" มันเป็น checkbox ใน console ที่เราไม่ได้เป็นเจ้าของ เราจึงปิดจากฝั่งเขาไม่ได้ เราเลยทำให้ฝั่งเราตอบรับไม่ได้แทน
routing key ทุกตัวที่เก็บไว้ถูกเติม prefix สี่ตัวอักษร key ที่มี prefix จะไม่ผ่าน validation ที่ ingest ไม่มี inbound webhook ตัวไหนจับคู่กับมันได้อีก และ request จะถูกปฏิเสธที่ขั้น signature verification เราเลือกเติม prefix แทนการ set column เป็น null ด้วยเหตุผลที่น่าเบื่อ: id เดิมยังรอดอยู่ในแถวนั้น ส่วน credentials ที่เข้ารหัสไว้ข้างๆ ไม่ถูกแตะเลย การกู้คืนทุกบัญชีใช้ statement เดียว (ชื่อในนี้ถูกเปลี่ยน):
UPDATE connected_accounts SET routing_key = substring(routing_key from 5) WHERE routing_key LIKE 'off:%';
สวิตช์ที่สาม: deploy script ที่รู้ว่าอะไรไม่มีอยู่
เรา deploy ผ่านสคริปต์ตัวเดียว ที่คำนวณว่า change หนึ่งกระทบแอปไหนบ้าง แล้ว ship เฉพาะแอปพวกนั้น serving plane กับ learning plane ใช้ package ร่วมกับ connector ที่ปลดระวางไป การแก้ shared code บรรทัดเดียวจึงเคยแปลว่า "deploy สี่อย่าง" หลังปลดระวาง มันจะแปลว่า "พยายาม deploy สองแอปที่ไม่มีอยู่แล้ว"
ตอนนี้สคริปต์มี dormant list สั้นๆ ติดตัว change detection ข้ามแอปพวกนั้นไปทั้งหมด จึงไม่มีอะไร ship มันไปโดยบังเอิญได้ routing rule ของมันยังอยู่ครบ และถ้าเรียกชื่อตรงๆ ก็ยัง deploy ได้ พร้อมบรรทัดเตือนว่าแอปต้องมีอยู่จริงและถือ secret ของมันก่อน ไม่เคยถูก detect แต่เรียกใช้ได้เสมอ นี่คือพฤติกรรมที่เราอยากได้จากของที่เก็บเข้าโกดัง
สวิตช์ที่สี่: โค้ดที่ยัง ship อยู่
business console ถูก build จาก image เดียวกับ web app หลัก หน้าเพจและ route ของมันจึงอยู่บน production จริงๆ ในตอนนี้ แต่ไม่ทำงาน ทุกอย่างในนั้นที่ทำอะไรแทนผู้ใช้ที่ sign in อยู่ จะถาม gate ตัวเดียวว่าแอปนี้คือ business app หรือเปล่า และ gate ตอบจาก origin ที่ตั้งค่าไว้ของ server เอง ไม่เคยตอบจาก request header header คือสิ่งที่คนแปลกหน้าพิมพ์เองได้ ส่วน configuration คือสิ่งที่เราเป็นคนตั้ง ค่า Host ที่สร้างสรรค์แค่ไหนก็ไม่เปลี่ยนอะไร และบนเว็บฝั่ง consumer path ของ console จะ redirect ออกไปก่อนที่อะไรในนั้นจะ render
สิ่งเดียวที่เราลบไม่ได้
credentials ที่เข้ารหัสไว้พวกนั้นยังเป็นแถวอยู่ใน database และมี encryption key ตัวเดียวที่ถอดรหัสมันได้ secret ตัวอื่นทุกตัวของแอปที่ปลดระวางหายไปพร้อมกับแอป key ตัวนั้นอยู่ต่อ พร้อมโน้ตบอกเหตุผล ถ้าทำมันหาย การปลดระวางนี้จะย้อนกลับไม่ได้อีกต่อไป การ cleanup ที่หวังดีลบ secret ที่ "ไม่ได้ใช้" ทิ้ง คือวิธีที่แผนทั้งหมดนี้จะพังได้สมจริงที่สุด มันจึงถูกเขียนไว้ในไฟล์ conventions ของ repo ซึ่งเป็นสิ่งแรกที่ใครก็ตามที่ทำงานกับมันต้องอ่าน
วางวิธีชุบชีวิตไว้ข้างศพ
config file ของแอปที่ปลดระวางแต่ละตัวยังอยู่ใน repo และตอนนี้แต่ละไฟล์เปิดหัวด้วย banner: DORMANT ถูกทำลายเมื่อวันที่เท่านี้ ไม่มีอะไร deploy ไฟล์นี้ ตามด้วยสูตรปลุกชีพ ห้าหรือหกขั้นตอนมีเลขกำกับ: สร้างแอปขึ้นใหม่ คืน secret ชี้ DNS กลับ ปลดสวิตช์ปิดตัวที่เกี่ยวข้อง deploy โดยเรียกชื่อ ของ OpenChat มีขั้นตอน "สแกน QR ใหม่" อยู่ด้วย ก็แน่ล่ะ ต้องมีอยู่แล้ว
banner ยังบอกด้วยว่าเก็บไฟล์ไว้ทำไม ไฟล์หนึ่งบันทึกว่า wiring กับ third party นั้น "แพงถ้าต้องค้นพบใหม่" อีกไฟล์อยู่เพื่อรักษากฎเครื่องเดียวเป็นหลัก และทุกไฟล์จบด้วยบรรทัดที่สำคัญที่สุดสำหรับคนอ่านที่เหนื่อยล้า: ให้ถือว่าทุกอย่างข้างล่างนี้เป็นคำบรรยายว่าแอปนี้ เคย ถูกต่อไว้อย่างไร ไม่ใช่ของที่เอาไปรัน
ถ้าเป็นหน้า wiki มันคงเพี้ยนจากความจริงไปนานแล้ว สูตรนี้อยู่ในไฟล์ที่ยังไงคุณก็ต้องเปิดอยู่ดี
สิ่งที่เราได้จากเรื่องนี้
- ลบ infrastructure เก็บโค้ด เขียนวิธีชุบชีวิตไว้ เครื่องคือส่วนที่แพงถ้าจะเก็บ และถูกถ้าจะสร้างใหม่ โค้ดกลับกัน
- Kill switch ที่ verify ไม่ได้คือคำอธิษฐาน ของเราคือแถวที่อ่านได้ key ที่จับคู่ไม่ติด list ในสคริปต์ และการเช็ก origin แต่ละตัวตรวจสอบแยกกันได้
- ชิ้นส่วนที่ขยับได้น้อยลงคือ feature สำหรับทีมคนเดียว มันอาจเป็น feature ตัวหลักเลยด้วยซ้ำ สามแอป ห้าเครื่อง และไม่มีใครต้องสแกน QR code ทุกวันพฤหัสฯ
ThaiPo ออกแบบ สร้าง และดูแลโดยนักพัฒนาคนเดียว Steven Acreman
ลองใช้ในแชทของคุณเอง
ThaiPo อยู่ใน LINE และแปลทุกข้อความให้สองทาง ฟรีตลอดไป เริ่มใช้ฟรี หรือ เพิ่มเพื่อน @thaipo.ai ใน LINE แล้วบอทจะส่งลิงก์สมัครให้ในแชทเลย