บทเรียน 09 — Production Operations: security, observability, rollback และต้นทุน
เวลาศึกษาอิสระสำหรับ full lesson: ประมาณ 4 ชั่วโมง · ในชั้น 8 ชั่วโมง: ใช้เฉพาะ operations/rollback overview ร่วมกับ deploy rehearsal · ระดับ: ผู้เรียนที่ผ่าน local smoke ในบทเรียน 08 · สถานะ: ตรวจเอกสาร Cloudflare ทางการและ starter repository ณ 9 กันยายน 2026
เป้าหมาย
ผู้เรียนจะเปลี่ยน project จาก “เปิด local URL ได้” เป็น release ที่ตรวจสอบและกู้คืนได้: deploy Worker + D1 demo จริง, ใช้ secret ถูกที่, เข้าใจ Turnstile production, ดู logs/metrics อย่างไม่รั่วข้อมูล, ทำ smoke test/rollback plan และติดตาม quota/cost driver. ผลงานสุดท้ายคือ runbook หนึ่งหน้าและ deployment evidence pack จริงจาก Step 7 ของบทเรียน 08; local/dry-run เพียงอย่างเดียวไม่จบหลักสูตร.
บทเรียนนี้ไม่สัญญาว่า Cloudflare ทำให้เว็บไซต์ปลอดภัยโดยอัตโนมัติ. Vibe coding สร้าง diff ได้เร็ว แต่ production ต้องมีมนุษย์รับผิดชอบ policy, data migration, privacy และสิทธิ์การ deploy ทุกครั้ง.
ก่อนเริ่ม
- ผ่าน
npm run db:local,npm run dev,npm run types,npm run typecheck,npm test,npm run deploy:checkและ Step 7 remote migration/deploy/smoke จากบทเรียน 08 - มี production URL, Worker version, D1 database ของตนเอง และ
DEMO_HMAC_KEYที่ provision ผ่าน secure stdin แล้ว; ห้ามส่งหรือบันทึกค่า secret wrangler.jsoncอยู่ใน Git;.dev.vars*/.env*ถูก ignore และ scan แล้วไม่มี secret ใน history ใหม่- เตรียม test case: asset page,
GET /api/catalog, availability, contact success/replay/origin rejection และ form production negative case (ในแผน ไม่ใช่ claim ว่ารันแล้ว) - กำหนดเจ้าของ release และวิธีติดต่อเมื่อเกิด incident แม้เป็นโปรเจกต์ผู้เรียน
ID ของ instructor กับ starter ที่ดาวน์โหลดได้
repository ที่ผู้สอน deploy อาจมี D1 database_id จริงเพื่อให้สาธิตและตรวจ URL ได้. นั่นเป็น resource ของ instructor, ไม่ใช่ resource ที่ผู้เรียนได้รับอนุญาตให้ deploy หรือ migrate. เมื่อส่ง source package ให้ผู้เรียน scripts/package-source.py แทน Cloudflare resource ID ด้วย student placeholders; ผู้เรียนต้องรัน wrangler whoami → สร้าง D1 ของตน → replace database_name/database_id → apply remote migration ตามบทเรียน 08. Placeholder ไม่ใช่ ID ที่ deploy ได้ และ ID ของ instructor ไม่ใช่ทางลัด.
รูปแบบ release ที่สอน
เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง
ภาพรวมชุดตรวจย่อยไว้ใน quality gates และไม่แสดงกิ่ง incident เพื่ออ่านได้ในเอกสาร ส่วนความสัมพันธ์จาก source เดิมยังต้องใช้ครบ: commit + review → build/typecheck/tests → local smoke → deploy staging → smoke URL จริงกับ logs → deploy production → metrics/logs/cost check; incident จึงพาไป rollback Worker code แล้วตามด้วย forward fix หรือ compatible DB migration ไม่มีกิ่งใดอ้างว่า rollback code ย้อนข้อมูล D1 ได้
ฝึกอ่านภาพ: เลือก failure หนึ่งแบบแล้วบอกว่าควรหยุดที่ gate ใด หลักฐานอะไรทำให้ตัดสินใจ rollback code ได้ และเหตุใด migration อาจต้องใช้ forward fix
ข้อเสนอแนะในการสอน: deployment ต้องมีขนาดเล็กและเปลี่ยนเรื่องเดียวในแต่ละครั้ง. อย่าใส่ UI ใหม่, migration แบบทำลายข้อมูล และเปลี่ยน authorization ใน release เดียว เพราะเมื่อ fail จะระบุสาเหตุและ rollback ลำบาก.
ขั้นตอนปฏิบัติ
1. วางแผน environment และ resource ก่อนเพิ่ม config
Step 7 ของบทเรียน 08 deploy top-level Worker + D1 production demo จริงก่อน. Named staging/production environment เป็นขั้นต่อยอดสำหรับทีมที่ต้องแยก release lane; ก่อนเพิ่ม environment ให้สร้าง resource จริงที่แยกกัน, ทำ config change ผ่าน review, และตั้งชื่อ เช่น vibe-to-production-academy-staging/...-production. ค่า non-secret เช่น DEMO_MODE เป็น vars ได้; secret ต้องตั้งแยกสำหรับทุก environment.
Cloudflare ระบุว่า named environment สร้าง Worker ชื่อแยก และ bindings กับ vars เป็น non-inheritable. ดังนั้น env.staging ต้องประกาศ D1 binding และ vars ของ staging ครบ, env.production ต้องประกาศของ production ครบ; อย่าชี้ทั้งสอง environment ไป D1 เดียวกัน (Wrangler environments). หลังเพิ่ม config, npm run types และ review output ก่อน deploy.
เฉพาะหลังจาก config/resource/secret ของ environment นั้นครบและได้รับสิทธิ์แล้ว จึงใช้ command trail นี้:
npx wrangler deploy --dry-run --env staging
npx wrangler deploy --env staging
# smoke test staging ที่ URL จริง
npx wrangler deploy --dry-run --env production
npx wrangler deploy --env production
ใช้ wrangler whoami เพื่อยืนยัน account ก่อนใช้คำสั่งที่เปลี่ยน production. ค่าลับ production ไม่ควรถูก download/copy ไป local เพื่อ “แก้สะดวก”.
Cloudflare Secrets เป็น encrypted binding; docs ระบุว่า local secret อยู่ใน .dev.vars หรือ .env และต้องไม่ commit (Workers Secrets). สำหรับ deployed demo, DEMO_HMAC_KEY เป็น required runtime secret: ตั้งหลัง initial deploy ด้วย secure-stdin command ในบทเรียน 08, ตรวจได้แค่ชื่อด้วย wrangler secret list, และ mutation forms ต้อง return fail-closed 503 จน secret พร้อม. เพิ่มขั้นตรวจใน release checklist: DEMO_HMAC_KEY, TURNSTILE_SECRET เมื่อ real mode ใช้ Turnstile, key ของ integration และ Access service token (ถ้ามี) อยู่ใน environment ถูกตัว แต่ห้ามพิมพ์ค่า secret ลง console/log/report.
2. ปกป้อง form และ write endpoint
ทุก endpoint ที่สร้าง contact/reservation/upload ต้องตรวจ method, content type, required fields, length/format, authorization และ Turnstile ก่อน INSERT, put หรือ action ภายนอก. Starter ทำ method/content/input/origin/idempotency/D1 checks แล้ว แต่ใน DEMO_MODE="true" จะข้าม Turnstile และ redact PII; จึงเป็น simulation, ไม่ใช่ความพร้อมสำหรับธุรกิจจริง. Demo rate metadata ใช้ HMAC ที่ day-scoped จาก CF-Connecting-IP, ไม่เก็บ IP ต้นฉบับและไม่ใช้ User-Agent เป็นตัวแยกสิทธิ์. Rate limiting policy ของธุรกิจจริงต้องมาจาก threat model ไม่ใช่ random number จาก AI.
Turnstile token จาก browser ต้องถูกส่งไป Worker แล้ว Worker เรียก Siteverify ด้วย secret. Cloudflare ระบุว่า server-side validation เป็น mandatory, token อายุ 300 วินาทีและใช้ครั้งเดียว (validate token). Failure ของ Siteverify คือ “อย่า mutate”; ทำข้อความให้ผู้ใช้เข้าใจได้, reset widget ถ้าหมดอายุ, และ log เฉพาะ error code/route/request ID ไม่ใช่ token หรือ form body.
เมื่อเพิ่ม frontend Turnstile widget, hostname, production secret และเปลี่ยน DEMO_MODE เป็น false แล้ว จึงเพิ่ม negative smoke tests เหล่านี้:
POST ไม่มี Turnstile token -> 400/403, ไม่มีแถวใหม่ใน D1
POST token ใช้ซ้ำ -> reject, ไม่มีแถวใหม่เพิ่ม
POST field เกิน limit -> 400, ไม่มี stack/SQL กลับสู่ browser
POST method ที่ไม่อนุญาต -> 405
Demo holds มีอายุ 30 นาทีโดยใช้วันเช่า UTC; cron 0 * * * * cleanup contact, quote, idempotency และ rate metadata ที่หมด retention 7 วัน. Scheduled cleanup มีความหน่วงได้ถึงรอบถัดไป และ availability/mutation cleanup hold ที่หมดอายุด้วยเพื่อไม่รอ cron เพียงอย่างเดียว. นี่เป็นเพียง demo policy; real mode ต้องกำหนด retention/deletion/access ใหม่ตามธุรกิจ. อ่าน demo data policy ก่อนแก้ code หรือเปิดรับข้อมูลจริง.
3. ใช้ Access ให้ตรงกับสิ่งที่ป้องกัน
วาง Cloudflare Access หน้า back office, preview deployment หรือ API สำหรับทีมที่ไม่ควร public. Access เป็น identity-aware proxy ที่ใช้ IdP, device posture และ policy checks; application ถูก deny by default จน match Allow policy (Access web applications). กำหนด policy เป็นกลุ่ม/role ที่ตั้งใจ, session duration ตามความเสี่ยง และทดสอบด้วยบัญชี allowed กับ denied.
Access ไม่แทน authorization ของลูกค้าใน public rental flow: Worker ยังต้องตรวจว่า customer ทำ action กับ resource ของตนได้เท่านั้น. สำหรับ Worker ให้ผูก Worker เป็น destination ของ Access application ซึ่ง Cloudflare ระบุว่าเป็นวิธีตรงไปตรงมาในการวาง authentication หน้าทุก route; สำหรับ origin ที่อยู่นอก Cloudflare ต้อง validate Access token เพื่อกันการ bypass (Choose an application type).
4. D1 data safety และการจองที่แข่งกัน
เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง
อ่านภาพนี้เป็นนโยบายตลอดอายุข้อมูล ไม่ใช่เพียง schema: ทุกช่วงต้องตอบได้ว่าเก็บอะไร ใครเข้าถึงเพื่ออะไร เก็บนานเท่าไร และตรวจการลบอย่างไร ค่า 30 นาที/7 วันในเดโมเป็นข้อเท็จจริงของเดโม ไม่ใช่นโยบายอัตโนมัติของธุรกิจจริง
ใช้ prepared statement + bind, unique key/idempotency key และ constraint ที่ธุรกิจกำหนด. ถ้าต้อง commit หลาย statement ให้ DB.batch() เป็นชุด transactional: เมื่อหนึ่ง statement fail จะ rollback sequence ทั้งหมด (D1 batch). อย่างไรก็ดี schema ต้องออกแบบให้ enforce กติกาการจอง และ handler ต้องตอบ duplicate/constraint failure เป็น error ที่ผู้ใช้เข้าใจ—not 500 ที่มี SQL dump.
ก่อน migration production ให้ทำตามนี้:
- อ่าน SQL และผลกระทบต่อข้อมูล, backup/export ตาม runbook ของโปรเจกต์
- apply บน local/staging ที่มี fixture ใกล้เคียง
- deploy code ที่อ่าน schema เดิมและใหม่ได้ (expand)
- apply production migration ที่ forward-compatible
- monitor error/latency และทำ backfill เป็นงานควบคุมได้
- ลบ column/constraint เก่าใน release หลัง เมื่อไม่มี code ใช้มัน
D1 migration ถูก track ใน d1_migrations; commit SQL และอย่าแก้ migration ที่ apply แล้ว (D1 migrations). D1 database หนึ่งตัวรับ query ทีละงานและอาจตอบ overloaded เมื่อ queue เต็ม; retry transient error ด้วย exponential backoff + jitter เท่านั้น (D1 limits, retry guidance). การ retry write ต้อง idempotent เสมอ.
5. Observability ที่ช่วย debug โดยไม่รั่วข้อมูล
starter เปิด Workers Logs อยู่แล้วและใช้ sampling 10% ที่ top-level:
{
"observability": { "enabled": true, "head_sampling_rate": 0.1 }
}
Workers Logs เก็บ invocation/custom/error/uncaught exception; docs แนะนำ log JSON แบบมี field เพื่อ filter/query ได้ และ head_sampling_rate ควบคุมสัดส่วน request ที่ log (Workers Logs). ต่อไปนี้เป็น event แบบปลอดภัยกว่า:
console.log(JSON.stringify({
event: "rental.submit", requestId, route: "/api/rentals",
outcome: "accepted", status: 201, durationMs
}));
ห้าม log: Authorization header, Cookie, Turnstile token, secret, full request/response body, password, email/phone ที่ไม่จำเป็น หรือ R2 presigned URL. ตั้ง requestId ที่ frontend/API/error log ใช้ตามกันได้. Cloudflare มี metrics สำหรับ request count, error rate, CPU/wall time และ execution duration; traces มี telemetry ของ fetch/binding operations (Observability).
Smoke หลัง deploy ให้ดูทั้ง user outcome และ dashboard/log: route 2xx/4xx/5xx, error spike, D1 duration, asset load, และ Access denied ที่คาดไว้. ใช้ wrangler tail เป็นเครื่องมือ debug ระยะสั้น ไม่ใช่ทดแทน dashboard/runbook หรือการเก็บเหตุการณ์ production ระยะยาว.
6. Rollback ที่เข้าใจขอบเขต
หลัง deploy Worker จริงแล้วเท่านั้น ให้บันทึก Worker version ก่อน production release, test release, แล้ว rehearsal code rollback บน staging:
npx wrangler versions list
npx wrangler rollback
ตรวจ syntax ที่ Wrangler รุ่นจริงรองรับก่อน execute. Rollback คืน Worker version ได้ แต่ไม่ undo D1/R2 state. Cloudflare เตือนว่า rollback ทำไม่ได้เมื่อ Developer Platform resource ถูกลบหรือถูกเปลี่ยนไม่เข้ากันระหว่าง version (Rollbacks). ด้วยเหตุนี้ migration destructive ไม่ใช่ “rollback button” และต้องมี forward fix/data recovery plan.
Runbook incident ขั้นต่ำ:
- หยุด release เพิ่มและบันทึกเวลา/version/error rate
- ยืนยันผลกระทบด้วย synthetic smoke + log/metrics โดยไม่เปิดเผย PII
- ถ้าเป็น code regression และ schema compatible ให้ rollback Worker แล้วทดสอบ URL/endpoint สำคัญ
- ถ้าเป็น data/migration issue ให้หยุด writes ตามความจำเป็น, ใช้ recovery plan ที่ผ่านการอนุมัติ, และทำ forward fix
- บันทึก root cause, affected version, corrective test และค่า monitor ที่จะป้องกันซ้ำ
7. วัด cost driver และ quota ก่อนกลายเป็น incident
อย่าตั้งใจจำราคา/limit จากสไลด์ เพราะ Free/Paid plan และหน้าราคาเปลี่ยนได้. ในวัน release ให้ผู้เรียนเปิด official pages ที่ account ใช้จริงและจด snapshot วันที่: Workers pricing, Workers limits, D1 pricing และ R2 pricing.
Checklist สำหรับ production:
| บริการ | ตัวขับต้นทุน/ข้อจำกัดที่ต้องดู | action ก่อนเสี่ยง |
|---|---|---|
| Worker | requests, CPU time, subrequests, static asset limits | ตั้ง CPU limit, ลดงาน CPU หนัก, ตรวจ build asset count/size |
| D1 | rows read/write, storage, query duration/concurrency | index จาก query จริง, pagination, batch ที่จำเป็น, idempotency |
| R2 | storage และ Class A/B operations | thumbnail/cache ที่เหมาะ, lifecycle/ลบไฟล์ orphan, อย่าทำ list/get ซ้ำโดยไม่จำเป็น |
| Logs | volume และ retention/sampling | structured JSON, sampling, ไม่ log body/PII |
Workers pricing แนะนำตั้ง CPU limit เพื่อลดความเสี่ยง runaway bill/denial-of-wallet; limit page เป็นแหล่ง authoritative สำหรับ request/body/binding/runtime constraints. สิ่งที่ขาดไม่ได้คือ alert/budget ที่ผู้รับผิดชอบ account ตั้งและทดสอบตาม plan ของตน—not a made-up number in source code.
แบบฝึกหัดและหลักฐานส่ง
- ส่ง Step 7 evidence: Worker URL/version, D1 name, remote migration, remote health/catalog/examples และ synthetic POST หลังตั้ง
DEMO_HMAC_KEY; ห้ามส่ง secret value. - สร้าง staging/prod config matrix: Worker name, D1 database, R2 bucket (ถ้าเพิ่ม), Access policy (ถ้าเพิ่ม), required secret name และ owner. ห้ามใส่ secret value.
- เขียน smoke checklist ที่ครอบ asset, health, catalog API, availability, demo contact success/replay/origin rejection,
503ก่อน secret provisioning และ production Turnstile negative tests ที่ยังต้องเพิ่ม. - จาก
wrangler.jsoncปัจจุบัน อธิบายว่า logs sampling 10% และ hourly cron อยู่ที่ใด และระบุ field ที่ safe/unsafe สำหรับ structured log. - เขียน rollback rehearsal plan สำหรับ staging ที่จะสร้างในอนาคต ของ code-only change. ห้ามใช้ rollback เพื่อทดสอบ destructive D1 migration.
- เขียน runbook 1 หน้า: trigger, owner, safe rollback condition, data-migration condition, communication และ post-incident action.
Troubleshooting
| อาการ | สาเหตุที่เป็นไปได้ | วิธีตอบสนอง |
|---|---|---|
| named environment รันไม่ได้ | config ยังไม่มี environment หรือ binding/vars ไม่ครบ | เพิ่ม config ที่ประกาศ non-inheritable binding/vars ครบ, รัน npm run types, review ก่อน deploy |
| form เพิ่มข้อมูลซ้ำ | retry/client double-submit ไม่มี idempotency | ใช้ idempotency key + unique constraint; ตอบผลเดิมอย่างปลอดภัย |
overloaded จาก D1 |
query/concurrency ต่อ database มาก | ลด/optimize query, pagination, retry transient with jitter, พิจารณา data partition ตามการออกแบบ |
| Access loop/403 | hostname/policy/IdP group หรือ cookie settings ผิด | ทดสอบ allowed/denied account, ตรวจ application hostname/policy, อย่าปิด Access ทั้งหมดเพื่อ “แก้เร็ว” |
| log เยอะหรือค้นหาไม่ได้ | log text ไม่มี field หรือ sampling ไม่เหมาะ | เปลี่ยนเป็น JSON structured event, ลด sensitive data, ปรับ sampling ตามข้อมูลจริง |
| rollback แล้วระบบยังผิด | schema/data ไม่ compatible กับ code เก่า | หยุดแผน code rollback, ใช้ forward fix/recovery runbook และตรวจ migration history |
เกณฑ์ผ่านบทเรียน
ผ่านเมื่อ Step 7 มี production URL/version, remote D1 migration, DEMO_HMAC_KEY provisioning โดยไม่เปิดเผยค่า, remote smoke และ synthetic POST; พร้อม config matrix, demo-data/retention understanding, structured-log policy, pricing/limits snapshot และ rollback runbook ที่ระบุชัดว่าครอบคลุม code ไม่ใช่ data rollback. Named staging/Access/R2 เป็น extension ตาม use case. ไม่มี account credential, ID ของ instructor หรือ dry-run อย่างเดียวคือ incomplete—not a deployed course website.