# บทเรียน 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 ที่สอน

![ลำดับประตูคุณภาพจาก lint และ typecheck ผ่าน API tests การตรวจ browser กับ accessibility build และ deploy dry-run ก่อน deploy และ production smoke โดยจุดใดไม่ผ่านให้หยุดและแก้](/diagrams/release-gates.svg)

[เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง](/diagrams/release-gates.html)

ภาพรวมชุดตรวจย่อยไว้ใน 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](https://developers.cloudflare.com/workers/wrangler/environments/)). หลังเพิ่ม config, `npm run types` และ review output ก่อน deploy.

เฉพาะหลังจาก config/resource/secret ของ environment นั้นครบและได้รับสิทธิ์แล้ว จึงใช้ command trail นี้:

```bash
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](https://developers.cloudflare.com/workers/configuration/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](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)). 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 เหล่านี้:

```text
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](../instructor/demo-data-policy.md) ก่อนแก้ 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](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/)). กำหนด 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](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/choose-application-type/)).

### 4. D1 data safety และการจองที่แข่งกัน

![วงจรชีวิตข้อมูลสาธิตตั้งแต่รับ request ตรวจและปกปิดข้อมูล เขียน D1 ใช้ตามวัตถุประสงค์ที่ประกาศ แล้วหมดอายุและถูก cleanup พร้อมแยกนโยบายจริงที่ต้องออกแบบใหม่เมื่อปิด demo mode](/diagrams/data-lifecycle.svg)

[เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง](/diagrams/data-lifecycle.html)

อ่านภาพนี้เป็นนโยบายตลอดอายุข้อมูล ไม่ใช่เพียง schema: ทุกช่วงต้องตอบได้ว่าเก็บอะไร ใครเข้าถึงเพื่ออะไร เก็บนานเท่าไร และตรวจการลบอย่างไร ค่า 30 นาที/7 วันในเดโมเป็นข้อเท็จจริงของเดโม ไม่ใช่นโยบายอัตโนมัติของธุรกิจจริง

ใช้ prepared statement + `bind`, unique key/idempotency key และ constraint ที่ธุรกิจกำหนด. ถ้าต้อง commit หลาย statement ให้ `DB.batch()` เป็นชุด transactional: เมื่อหนึ่ง statement fail จะ rollback sequence ทั้งหมด ([D1 batch](https://developers.cloudflare.com/d1/worker-api/d1-database/)). อย่างไรก็ดี schema ต้องออกแบบให้ enforce กติกาการจอง และ handler ต้องตอบ duplicate/constraint failure เป็น error ที่ผู้ใช้เข้าใจ—not 500 ที่มี SQL dump.

ก่อน migration production ให้ทำตามนี้:

1. อ่าน SQL และผลกระทบต่อข้อมูล, backup/export ตาม runbook ของโปรเจกต์
2. apply บน local/staging ที่มี fixture ใกล้เคียง
3. deploy code ที่อ่าน schema เดิมและใหม่ได้ (expand)
4. apply production migration ที่ forward-compatible
5. monitor error/latency และทำ backfill เป็นงานควบคุมได้
6. ลบ column/constraint เก่าใน release หลัง เมื่อไม่มี code ใช้มัน

D1 migration ถูก track ใน `d1_migrations`; commit SQL และอย่าแก้ migration ที่ apply แล้ว ([D1 migrations](https://developers.cloudflare.com/d1/reference/migrations/)). D1 database หนึ่งตัวรับ query ทีละงานและอาจตอบ `overloaded` เมื่อ queue เต็ม; retry transient error ด้วย exponential backoff + jitter เท่านั้น ([D1 limits](https://developers.cloudflare.com/d1/platform/limits/), [retry guidance](https://developers.cloudflare.com/d1/best-practices/retry-queries/)). การ retry write ต้อง idempotent เสมอ.

### 5. Observability ที่ช่วย debug โดยไม่รั่วข้อมูล

starter เปิด Workers Logs อยู่แล้วและใช้ sampling 10% ที่ top-level:

```jsonc
{
  "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](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)). ต่อไปนี้เป็น event แบบปลอดภัยกว่า:

```ts
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](https://developers.cloudflare.com/workers/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:

```bash
npx wrangler versions list
npx wrangler rollback
```

ตรวจ syntax ที่ Wrangler รุ่นจริงรองรับก่อน execute. Rollback คืน Worker version ได้ แต่ไม่ undo D1/R2 state. Cloudflare เตือนว่า rollback ทำไม่ได้เมื่อ Developer Platform resource ถูกลบหรือถูกเปลี่ยนไม่เข้ากันระหว่าง version ([Rollbacks](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/)). ด้วยเหตุนี้ migration destructive ไม่ใช่ “rollback button” และต้องมี forward fix/data recovery plan.

Runbook incident ขั้นต่ำ:

1. หยุด release เพิ่มและบันทึกเวลา/version/error rate
2. ยืนยันผลกระทบด้วย synthetic smoke + log/metrics โดยไม่เปิดเผย PII
3. ถ้าเป็น code regression และ schema compatible ให้ rollback Worker แล้วทดสอบ URL/endpoint สำคัญ
4. ถ้าเป็น data/migration issue ให้หยุด writes ตามความจำเป็น, ใช้ recovery plan ที่ผ่านการอนุมัติ, และทำ forward fix
5. บันทึก root cause, affected version, corrective test และค่า monitor ที่จะป้องกันซ้ำ

### 7. วัด cost driver และ quota ก่อนกลายเป็น incident

อย่าตั้งใจจำราคา/limit จากสไลด์ เพราะ Free/Paid plan และหน้าราคาเปลี่ยนได้. ในวัน release ให้ผู้เรียนเปิด official pages ที่ account ใช้จริงและจด snapshot วันที่: [Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/), [Workers limits](https://developers.cloudflare.com/workers/platform/limits/), [D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/) และ [R2 pricing](https://developers.cloudflare.com/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.

## แบบฝึกหัดและหลักฐานส่ง

1. ส่ง Step 7 evidence: Worker URL/version, D1 name, remote migration, remote health/catalog/examples และ synthetic POST หลังตั้ง `DEMO_HMAC_KEY`; ห้ามส่ง secret value.
2. สร้าง staging/prod config matrix: Worker name, D1 database, R2 bucket (ถ้าเพิ่ม), Access policy (ถ้าเพิ่ม), required secret name และ owner. ห้ามใส่ secret value.
3. เขียน smoke checklist ที่ครอบ asset, health, **catalog** API, availability, demo contact success/replay/origin rejection, `503` ก่อน secret provisioning และ production Turnstile negative tests ที่ยังต้องเพิ่ม.
4. จาก `wrangler.jsonc` ปัจจุบัน อธิบายว่า logs sampling 10% และ hourly cron อยู่ที่ใด และระบุ field ที่ safe/unsafe สำหรับ structured log.
5. เขียน rollback rehearsal plan **สำหรับ staging ที่จะสร้างในอนาคต** ของ code-only change. ห้ามใช้ rollback เพื่อทดสอบ destructive D1 migration.
6. เขียน 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.
