GĐ17 — Cloudflare cho Backend: Workers, R2, D1, KV, Queues, Durable Objects, DNS/CDN
Giai đoạn này đưa một API nhỏ lên Cloudflare, nối compute với dữ liệu và file, xử lý việc nền, rồi quan sát request ở production. Mục tiêu là hiểu cách chọn đúng sản phẩm và ranh giới của chúng — không phải học thuộc toàn bộ danh mục Cloudflare.
Kiểm chứng ngày 2026-10-05: giới hạn Workers đọc từ Workers limits, giới hạn D1 từ D1 limits, cờ
nodejs_compattừ Node.js compatibility. Đã chạy trong thư mục tạm bằng wrangler 4.147.0 và TypeScript 7.0.2 (tsc --strictsạch): Durable Object, Cron Trigger,batch()của D1, kiểm kích thước R2 và tạo instance Workflow trênwrangler devlocal, cùngwrangler deploy --dry-runcho cấu hình KV vàenv.staging. Chưa chạy trên tài khoản Cloudflare thật: deploy,wrangler tail, versions và rollback. Con số giới hạn đổi theo thời gian, nên luôn đối chiếu lại trang tài liệu trước khi dựa vào chúng.
1. Sau giai đoạn này, bạn làm được gì?#
- Giải thích request đi từ DNS/proxy đến cache, Worker và binding dữ liệu như thế nào.
- Viết được một Worker API bằng TypeScript và chạy thử bằng Wrangler.
- Chọn đúng nơi lưu: R2 cho object/file, D1 cho dữ liệu quan hệ, KV cho dữ liệu đọc nhiều và chấp nhận eventual consistency.
- Đưa công việc khỏi request bằng Queues, có retry, DLQ và xử lý lặp an toàn.
- Nhận ra lúc nào cần Durable Objects để điều phối một trạng thái dùng chung.
- Phân biệt quyền tài khoản Cloudflare, API token, secret của ứng dụng và binding của Worker.
- Đọc log/trace, giới hạn cache đúng cách, triển khai staging rồi mới production và dọn tài nguyên lab.
Giai đoạn trước đã giúp bạn hiểu Docker, CI/CD và AWS. Giai đoạn này so sánh các ý tưởng tương đương để dễ định hướng, nhưng không coi sản phẩm hai nền tảng là thay thế 1:1.
| Nhu cầu | Cloudflare thường dùng | Gần với AWS ở khía cạnh | Khác biệt cần nhớ |
|---|---|---|---|
| Chạy API / request handler | Workers | Lambda | Worker chạy trên runtime web/edge; không phải máy Linux luôn bật như EC2 |
| File và object | R2 | S3 | R2 có API S3-compatible và binding riêng; quyền truy cập công khai cần cấu hình có chủ đích |
| SQL | D1 | RDS ở góc nhìn “database có quản lý” | D1 là SQLite serverless, không phải PostgreSQL/RDS thu nhỏ tương đương |
| Đọc key-value rất nhiều | Workers KV | Một phần use case DynamoDB/cache | KV eventually consistent; không dùng làm nguồn sự thật cần transaction |
| Việc nền | Queues | SQS ở khái niệm hàng đợi | Tin nhắn có thể giao lặp; consumer phải idempotent |
| Trạng thái cần điều phối | Durable Objects | Một phần use case stateful service/lock | Mỗi object là một điểm điều phối có tên và storage riêng, mạnh về một nhóm trạng thái cụ thể |
| DNS, proxy, phân phối nội dung | DNS, CDN/Cache, Rules | Route 53 + CloudFront ở một số lớp | Cache phụ thuộc proxy, header và rule; DNS không tự làm ứng dụng an toàn |
Cloudflare không cung cấp một bản sao VPC theo cách AWS tổ chức VPC/subnet/security group. Nếu cần Worker truy cập database riêng tư đang nằm trong AWS/on-prem, xem Hyperdrive + Workers VPC/Tunnel; đó là kết nối tới mạng/database hiện có, không phải thay VPC AWS.
2. Mô hình request: edge, Worker, binding#
Với domain được proxy qua Cloudflare, request thường đi theo các bước sau:
Binding là cách Worker truy cập dịch vụ Cloudflare bằng một biến runtime như env.DB hoặc env.FILES. Binding giúp ứng dụng gọi resource qua API đã định nghĩa, không cần tự gọi Cloudflare REST API hay nhúng credential quản trị vào code. Tên biến binding do bạn đặt trong cấu hình Wrangler.
Static asset có thể được trả trực tiếp mà không gọi Worker. Với Workers Static Assets, request khớp file tĩnh thường được phục vụ trước; các route API hoặc request không khớp mới đi vào Worker theo cấu hình. Đây là cách ghép site tĩnh và API trong cùng ứng dụng.
Domain DNS-only chỉ dùng DNS để phân giải tên; request không đi qua Cloudflare proxy nên không tự được CDN/cache/WAF của Cloudflare xử lý. Muốn áp dụng tính năng edge cho host đó, record cần được proxy và sản phẩm/rule tương ứng phải được bật.
Tài liệu: Workers overview, bindings, Static Assets.
3. Workers — compute cho request và API#
Worker là gì?#
Worker là code xử lý request/event chạy trên runtime của Cloudflare. Với API, entry point thường nhận Request, env chứa binding/biến môi trường, rồi trả Response. Nhiều API web tiêu chuẩn như fetch, Request, Response, URL, crypto hoạt động tự nhiên.
Worker không phải Express server chạy mãi trên một VM. Không thiết kế dựa trên process sống lâu, file cục bộ bền vững, socket nghe cổng hoặc cache trong bộ nhớ dùng chung cho mọi request. Dữ liệu cần tồn tại phải đặt trong binding/store phù hợp. Nếu code Node cần module hoặc API runtime cụ thể, kiểm tra khả năng tương thích Node. Với compatibility_date từ 2026-08-04 trở đi, Workers bật sẵn nodejs_compat; với ngày từ 2024-09-23 đến 2026-08-03 phải tự thêm "compatibility_flags": ["nodejs_compat"] (tài liệu, đọc 2026-10-05). Tương thích Node không biến Worker thành máy chủ Node Linux đầy đủ.
Worker API tối thiểu#
Ví dụ sau có route health check và route tạo ghi chú. Nó minh họa parse URL, kiểm tra method/input và bind tham số SQL; chưa bao gồm đăng nhập hay authorization.
Vì sao dùng prepared statement? ? là chỗ giữ tham số, .bind(...) đưa dữ liệu riêng vào query. Đừng ghép input thành chuỗi SQL. Trong app thật, thêm authentication và authorization trước khi đọc/ghi tài nguyên của user; validation không thay thế phân quyền.
Wrangler và môi trường#
Wrangler là CLI phát triển/deploy Worker. Cấu hình mới nên theo format wrangler.jsonc và được xem là source of truth cho tên Worker, compatibility date, bindings, observability và môi trường.
Chọn template Worker TypeScript để bài lab bắt đầu từ API đơn giản. wrangler dev chạy môi trường local; trước khi dùng binding remote, hãy kiểm tra cấu hình — local code vẫn có thể được nối tới resource thật nếu bật remote binding.
Tách staging và production bằng Wrangler environments hoặc project/deployment riêng. Mỗi môi trường phải gắn đúng database, bucket, queue, route và secret. Không để staging dùng nhầm production database. Sau khi thêm binding, chạy npx wrangler types để sinh kiểu TypeScript theo cấu hình thật.
Giới hạn cần biết trước khi thiết kế#
| Giới hạn | Workers Free | Workers Paid |
|---|---|---|
| Request mỗi ngày | 100.000 | không giới hạn |
| CPU time mỗi request HTTP | 10 ms | mặc định 30 giây, tối đa 5 phút |
| Bộ nhớ | 128 MB | 128 MB |
| Subrequest mỗi request | 50 | 10.000 |
| Số Worker mỗi tài khoản | 100 | 500 |
| Kích thước bundle (chưa nén) | 64 MiB | 64 MiB |
Nguồn: Workers limits, đọc 2026-10-05. Hai điểm hay hiểu nhầm: giới hạn là CPU time, không phải thời gian chờ I/O (chờ fetch, KV hay truy vấn database không tính vào CPU time), và ctx.waitUntil() chỉ kéo dài việc thêm tối đa 30 giây sau khi response đã gửi. Việc nặng hơn thế đi qua Queues hoặc Workflows (cuối mục 9).
Cấu hình KV, môi trường staging và Durable Object#
Binding và biến môi trường không kế thừa từ cấu hình gốc vào env.<tên>: mỗi môi trường phải khai báo lại, nếu không staging có thể thiếu binding hoặc dùng nhầm tài nguyên (Environments, đọc 2026-10-05). Mẫu rút gọn:
Đã chạy npx wrangler deploy --dry-run --config <file> --env staging với id giả: binding CACHE trỏ đúng id staging và APP_ENV là "staging"; không có --env thì CACHE trỏ id gốc. compatibility_date này từ 2026-08-04 trở đi nên không cần cờ nodejs_compat. Deploy staging dùng npx wrangler deploy --env staging; Worker khi đó tên private-docs-api-staging. Bài lab ở mục 10 dựng đủ D1, R2 và queue riêng cho staging.
4. Workers Static Assets, Pages, DNS và CDN#
Site tĩnh kết hợp API#
Workers Static Assets cho phép deploy static files cùng Worker trong một lần. Ví dụ site có /index.html, /assets/app.js và /api/*: file khớp asset có thể được phục vụ từ CDN; route API chạy Worker.
Cloudflare Pages cũng có thể host site và Pages Functions. Backend Guide hiện đang chạy trên Pages, vì vậy dùng repo này làm ví dụ đọc cấu hình/build/deploy là hợp lý; giai đoạn học không yêu cầu chuyển dự án hiện tại sang Workers. Với project mới, trang Pages của Cloudflare khuyến nghị bắt đầu bằng Workers ("Start new projects with Workers"), còn Pages không bị gỡ; project Pages có sẵn chuyển sang Workers theo hướng dẫn migrate from Pages (trang Pages, đọc 2026-10-05). Xem thêm Workers Static Assets và framework adapter đang dùng trước khi chọn cách deploy.
Cache đúng nội dung#
Cache giảm request về origin và thời gian phản hồi, nhưng response chứa dữ liệu theo user không được cache như asset công khai. Với dữ liệu cá nhân, đặt chính sách rõ như Cache-Control: private, no-store; kiểm tra Set-Cookie, Authorization, cache key và rule trước khi bật “cache everything”. Cloudflare không mặc định cache HTML/JSON trong nhiều cấu hình CDN, nhưng Cache Rules có thể thay đổi hành vi đó.
Ví dụ phân loại:
| URL | Cách xử lý |
|---|---|
/assets/app.abc123.js | Asset có hash, cache dài hạn; khi nội dung đổi thì tên file đổi |
/api/catalog | Chỉ cache nếu response thực sự dùng chung, có TTL và cách purge/đổi version |
/api/me | Theo user; thường trả private, no-store, không dùng cache chung |
/api/documents/:id | Kiểm tra quyền user trước khi đọc object; không suy ra quyền từ URL/key |
Tài liệu: Cloudflare Cache, default cache behavior, Cache Rules, Static Assets routing.
5. R2 — lưu object và file#
Khi dùng R2?#
Dùng R2 cho file do người dùng tải lên, ảnh, export, bản sao lưu hoặc artifact lớn. Đây là object storage: mỗi object có key, metadata và bytes; nó không phải filesystem để ứng dụng tùy ý rename/thực hiện transaction thư mục.
Worker dùng R2 binding để thao tác trực tiếp. Bucket private là mặc định tốt cho file user; chỉ mở đường dẫn tải sau khi API xác thực quyền, hoặc phát hành URL có scope/thời hạn phù hợp. Không trả objectKey công khai rồi cho rằng nó bí mật là đủ bảo vệ file.
Ví dụ upload qua Worker#
Đã chạy đoạn kiểm kích thước này trên wrangler dev local: body nhỏ trả 201, body 11 MB trả 413, request không có Content-Length (chunked) trả 411. userId trong ví dụ phải lấy từ identity đã xác thực, không lấy trực tiếp từ body/path do client gửi. Ngoài kích thước, kiểm tra loại file theo allowlist/magic bytes phù hợp, áp quota, chống upload lặp và không ghi file nhạy cảm vào log. Nếu cần file lớn, hỗ trợ multipart/resumable flow thay vì buffer toàn bộ file vào RAM.
Metadata và vòng đời file#
Thông tin cần query như owner, trạng thái quét, thời gian tạo và liên kết domain nên nằm trong D1/SQL; bytes nằm trong R2. Ghi file và ghi DB không phải một transaction chung. Thiết kế compensation/cleanup: nếu upload thành công nhưng insert DB lỗi thì xóa object hoặc để job đối soát tìm orphan; nếu DB record được tạo nhưng object bị xóa, đánh dấu lỗi và phục hồi/xóa metadata theo quy trình.
Worker download phải kiểm tra quyền trên metadata trước khi get(key). Trả Content-Type đã kiểm tra, X-Content-Type-Options: nosniff, và cân nhắc Content-Disposition: attachment cho file user. Nếu cần giao file trực tiếp qua S3-compatible API, tìm hiểu riêng credentials/scope và presigned URL; API binding trong Worker và S3-compatible API không hoàn toàn giống nhau.
Tài liệu: Use R2 from Workers, R2 Workers API reference.
6. D1 và Hyperdrive — chọn database theo workload#
D1: SQL qua binding#
D1 là database SQL serverless của Cloudflare, dựa trên SQLite và được truy cập từ Worker qua binding. Đây là cách dễ bắt đầu cho ứng dụng/lab chạy gần Worker. D1 có migration, prepared query và transaction semantics riêng; đọc giới hạn/consistency/concurrency trước khi chọn cho workload lớn hoặc nhiều ghi đồng thời.
Ví dụ migration migrations/0001_create_notes.sql:
Cấu hình Worker gắn D1 bằng tên binding, tên database và ID database do Cloudflare tạo. Trong code, dùng env.DB.prepare(...).bind(...).run() như ví dụ ở phần Workers. Tạo migration để thay đổi schema có thể review và chạy lặp theo môi trường; không sửa production bằng câu lệnh ad-hoc rồi quên đưa thay đổi vào source control.
Chỉ chạy migration --remote sau khi xác nhận binding, tài khoản và database đích. Tên database trong lệnh phải đúng với cấu hình Wrangler.
D1: giới hạn và transaction bằng batch#
Giới hạn đáng nhớ (D1 limits, đọc 2026-10-05):
| Giới hạn | Free | Paid |
|---|---|---|
| Dung lượng mỗi database | 500 MB | 10 GB |
| Query mỗi lần invoke Worker | 50 | 1.000 |
| Tham số bind mỗi query | 100 | 100 |
| Độ dài câu SQL | 100 KB | 100 KB |
| Kích thước một dòng, chuỗi hoặc BLOB | 2 MB | 2 MB |
100 tham số bind nghĩa là INSERT nhiều dòng một lần với ? cho từng cột sẽ vượt giới hạn khi số dòng nhân số cột quá 100: chia nhóm hoặc dùng batch(). Vì trần mỗi database là 10 GB, hãy cân nhắc chia dữ liệu thành nhiều database nhỏ (ví dụ mỗi tenant một database) khi dự đoán vượt trần, thay vì dồn vào một database.
Khi cần nhiều thay đổi nguyên tử, dùng batch(): tài liệu mô tả các câu trong batch là một transaction SQL, thực thi tuần tự và không song song, câu nào lỗi thì cả chuỗi bị huỷ và rollback (D1 Database API, đọc 2026-10-05).
Đã chạy trên wrangler dev local với bảng quota có CHECK (used <= 1): câu thứ hai vi phạm ràng buộc, batch ném lỗi CHECK constraint failed và truy vấn lại cho thấy dòng của câu thứ nhất không còn (0 dòng). Dùng exec() cho việc bảo trì và migration một lần, không cho luồng nghiệp vụ: tài liệu nói rõ khi lỗi nó chỉ dừng, không nói về rollback. Việc tự mở transaction bằng BEGIN/COMMIT qua D1: chưa xác minh trong tài liệu, vì vậy đừng dùng; batch() là đường được ghi lại.
Hyperdrive: giữ Postgres/MySQL hiện có#
Nếu dự án đã dùng PostgreSQL/MySQL — ví dụ RDS ở GĐ16 — không cần chuyển database sang D1 để chạy Worker. Hyperdrive cho Worker kết nối tới database hiện có, quản lý pool gần database và tối ưu kết nối từ mạng edge. Nó giữ vai trò cầu nối/tối ưu truy cập, không biến RDS thành D1 và cũng không xóa nhu cầu cấu hình firewall, credential, backup, schema migration hay connection budget.
Chọn theo câu hỏi:
- Schema và query có phù hợp SQLite, workload gọn và muốn dùng database native của Workers? Thử D1.
- Hệ thống đã có Postgres/MySQL, cần ORM/driver hoặc migration đang dùng? Giữ DB và cân nhắc Hyperdrive.
- Có yêu cầu transaction, extension, query pattern, backup/restore hoặc workload vượt giới hạn dịch vụ? Đo workload và so sánh database managed trước khi quyết định.
Tài liệu: D1 getting started, D1 Wrangler commands, Hyperdrive overview, kết nối database trong Workers.
7. KV và Durable Objects — key-value phân tán hay điều phối trạng thái?#
Workers KV: đọc nhiều, ghi tương đối ít#
KV lưu cặp key-value và cache dữ liệu tại edge. Hợp với cấu hình, feature flags, nội dung đọc nhiều, lookup gần như tĩnh hoặc cache có TTL. Khi key được ghi ở một vùng, thay đổi có thể mất thời gian mới nhìn thấy ở vùng khác: KV eventually consistent. Vì vậy không dùng nó làm nguồn sự thật cho số dư tiền, khóa duy nhất, revocation phải tức thời hay cập nhật cần read-your-writes toàn cầu.
Ví dụ cache cấu hình công khai:
Cache này chấp nhận stale tối đa một khoảng ngắn. Tăng version key khi đổi format (catalog:v4), đặt TTL hợp với độ tươi, và có chiến lược fallback nếu cache miss hoặc binding lỗi.
Durable Objects: một điểm xử lý cho một nhóm trạng thái#
Durable Object có tên định danh duy nhất, storage gắn với object và xử lý tuần tự các yêu cầu tới cùng instance. Dùng khi nhiều request/client phải phối hợp trên cùng một trạng thái: phòng chat, phiên cộng tác, bộ đếm giới hạn theo khóa, presence hoặc lock có vòng đời rõ.
Ví dụ, tạo một object theo roomId; mọi message của phòng đó được đưa về cùng object để cấp thứ tự và broadcast. Các phòng khác nhau vẫn phân tán độc lập. Lưu dữ liệu cần tồn tại trong storage bền, đừng chỉ dựa vào biến RAM vì object có thể idle và khởi tạo lại.
Không dùng Durable Object mặc định cho mọi cache hoặc DB. Mỗi object thường là một vùng điều phối riêng; thiết kế khóa kém (ví dụ một object toàn hệ thống cho mọi tenant) có thể tạo hotspot. Chọn key để chia tải và giới hạn tác động nếu một object chậm/hỏng.
Durable Object tối thiểu: bộ đếm theo khóa#
Bộ đếm rate limit theo userId: mỗi khóa là một Durable Object riêng, nên hai request cùng khóa được xử lý lần lượt và con số không bị đua. Cấu hình ở phần "Cấu hình KV, môi trường staging và Durable Object" (mục 3): class nằm ở exports kèm "storage": "sqlite", còn durable_objects.bindings đặt tên biến env.RATE_LIMITER.
Cú pháp class, exports và getByName theo Durable Objects: get started (đọc 2026-10-05). Đã chạy trên wrangler dev local: với limit 3 trong cửa sổ 60 giây, ba lần gọi đầu cùng khóa trả allowed: true (còn 2, 1, 0), lần thứ tư trả allowed: false, còn khóa khác vẫn allowed: true. Chạy npx wrangler types sau khi đã có class để env.RATE_LIMITER có kiểu DurableObjectNamespace<RateLimiter>; chạy trước thì stub.hit báo lỗi kiểu. Một khóa là một object nên chọn khóa theo user hay phòng, đừng dùng một khóa toàn hệ thống.
Tài liệu: KV consistency và use cases, Durable Objects.
8. Queues — xử lý việc nền và giao nhận lặp an toàn#
Đưa việc chậm hoặc có thể retry khỏi request chính. Ví dụ: sau khi file vào R2, Worker ghi metadata và gửi message scan-upload; consumer quét virus/trích metadata rồi cập nhật D1. Request upload trả về sớm với trạng thái processing.
Queues cung cấp batching, retry/delay và DLQ. Delivery mặc định là at-least-once: một message có thể tới consumer nhiều hơn một lần. Vì thế:
- Payload nhỏ, có
jobId,schemaVersionvà chỉ chứa dữ liệu cần thiết; không nhét secret/PII dư thừa. - Consumer ghi kết quả bằng idempotency key hoặc unique constraint trong DB.
- Phân loại lỗi tạm thời (retry) và lỗi vĩnh viễn (đưa DLQ/đánh dấu fail).
- Theo dõi độ sâu queue, tuổi message cũ nhất, retry và DLQ; có quy trình replay sau khi sửa lỗi.
- Đặt timeout và batch size theo thời gian xử lý/giới hạn dependency, không chỉ chọn số lớn.
Ví dụ consumer ở mức minh họa:
Trong code production, không log raw body hoặc token. processUploadOnce phải thiết kế sao cho retry sau crash không gửi email, tính phí hoặc tạo record trùng. Việc gọi API ngoài cũng nên truyền idempotency key nếu provider hỗ trợ.
Tài liệu: Queues overview, delivery guarantees, how Queues works.
9. Quyền, secrets, log và chi phí#
Ba loại quyền không được nhầm#
- Account/API token: quyền của người hoặc CI khi quản trị Cloudflare. Tạo token theo đúng account, resource và action tối thiểu; tách token CI khỏi token cá nhân, đặt hạn dùng và có quy trình thu hồi.
- Worker binding: quyền runtime tới đúng dịch vụ mà Worker được gắn, như bucket R2, database D1 hoặc namespace KV. Không đưa account API token vào request handler để truy cập dịch vụ nội bộ.
- Secret ứng dụng: credential tới dịch vụ ngoài, ví dụ webhook signing key. Lưu bằng secret mechanism của Workers/Wrangler, không commit
.dev.vars, không in secret vào log.
Observability#
Ghi log có cấu trúc để lọc theo route, event, request id và kết quả. Ví dụ:
Không ghi access token, URL có chữ ký, nội dung file hoặc dữ liệu cá nhân không cần thiết. Bật Workers Logs/observability theo cấu hình, xem invocation/error logs, kiểm tra sampling và retention. Với nhiều request, đo tỉ lệ lỗi và latency theo route; “deploy thành công” chưa chứng minh API hoạt động.
Chi phí và cleanup#
Tính giá hiện tại trong Cloudflare pricing/calculator trước khi tạo tài nguyên thật. Chi phí có thể đến từ request compute, storage, thao tác đọc/ghi, log ingestion, database, queue và dịch vụ add-on. Đặt budget/alert nếu tài khoản hỗ trợ, dùng resource lab riêng, giới hạn upload, log sampling phù hợp và dọn resource không dùng. Free plan/limit thay đổi theo thời gian — không dùng con số đọc từ bài blog cũ làm cam kết.
Tài liệu: Wrangler secrets, environments, Workers Logs, Workers best practices, pricing.
Workflows, Cron Triggers, tail log và rollback#
Cron Trigger chạy hàm scheduled theo lịch, tính theo UTC (Cron Triggers, đọc 2026-10-05). Khai báo "triggers": { "crons": ["0 3 * * *"] } trong wrangler.jsonc, thêm handler cạnh fetch:
Chạy thử local bằng curl "http://localhost:8787/cdn-cgi/local/scheduled" khi wrangler dev đang chạy (đã chạy: handler được gọi; không truyền tham số cron thì controller.cron rỗng). Viết hàm idempotent để một lần chạy lại không nhân đôi kết quả.
Workflows dành cho chuỗi bước dài có retry và chờ (onboarding gửi email sau một ngày, quy trình duyệt). Mỗi step.do lưu kết quả, nên khi lỗi hoặc restart, workflow chạy tiếp từ bước đã xong thay vì làm lại từ đầu (Workflows: get started, đọc 2026-10-05):
Binding khai báo "workflows": [{ "name": "onboarding", "binding": "ONBOARDING", "class_name": "Onboarding" }], và Worker tạo instance bằng env.ONBOARDING.create({ params: { userId } }). Đã chạy tsc --strict cho class này và tạo instance thành công trên wrangler dev; chưa chờ qua bước sleep một ngày nên email giả chưa chạy. Chọn Queues khi chỉ cần giao việc rời rạc, Workflows khi các bước phụ thuộc nhau và cần chờ.
Quan sát và quay lui (lệnh Wrangler theo Workers commands, đọc 2026-10-05; chưa chạy vì cần tài khoản Cloudflare):
Tách versions upload (không đổi traffic) khỏi versions deploy cho phép phát hành dần; rollback triển khai ngay một version đã có. Với versions deploy, tài liệu nêu tham số [VERSION-SPECS] và cờ --percentage; cú pháp chia traffic cụ thể chưa xác minh, hãy đọc npx wrangler versions deploy --help trước khi dùng. Rollback chỉ lùi code và cấu hình của Worker; migration D1 đã áp, dữ liệu R2 và message trong queue không tự lùi.
10. Bài lab — private document API trên Cloudflare#
Mục tiêu#
Build một API nhỏ để user upload tài liệu riêng tư: Worker xác thực request, bytes vào R2, metadata vào D1, job quét file gửi qua Queue, rồi endpoint chỉ cho owner tải lại. Thêm KV chỉ cho dữ liệu danh mục công khai; thêm Durable Object như bài mở rộng để điều phối quota theo tenant hoặc phiên xử lý. Không cần ép cả bảy sản phẩm vào nếu use case không đòi hỏi.
Bước 1: khởi tạo và mô hình dữ liệu#
-
Tạo Worker TypeScript bằng
npm create cloudflare@latestvà chạynpx wrangler dev.Đáp án
Lệnh trong terminal (chưa chạy;
--localchỉ dùng trạng thái giả lập trong.wrangler/):bashReady -
Tạo D1 database và R2 bucket riêng cho lab; bind vào Worker bằng tên rõ như
DBvàFILES.Đáp án
Cấu hình
wrangler.jsonc(bindingDB,FILES,SCAN_QUEUE; consumer cùng Worker; DLQ khai báo tường minh vì không có DLQ thì message hết retry bị xoá).database_idthật chỉ có sauwrangler d1 create; chạy--localkhông cần.jsonReadyHai queue (
scan-uploadvà DLQscan-upload-dlq) phải tồn tại trước khi deploy. Tạo bằngnpx wrangler queues create scan-uploadvànpx wrangler queues create scan-upload-dlq(lệnh tác động tài khoản thật, chưa chạy); chạy--localthì không cần. -
Tạo migration có bảng
documents(id, owner_id, object_key, content_type, byte_size, status, created_at); thêm unique constraint choobject_keyvà index theo(owner_id, created_at).Đáp án
migrations/0001_create_documents.sql(thêmscan_log(job_id PRIMARY KEY)làm bản ghi idempotency cho bước 3):textReady -
Thêm middleware/auth layer; user ID lấy từ token/session đã xác minh, không tin header tự gửi từ browser.
Đáp án
Lab dùng bảng token giả đọc từ secret
TOKENS(thực tế verify JWT/session). ChỉuserIdtừ bảng này được dùng, không đọc từ header do client đặt:typescriptReadyfetch()gọi hàm này trước mọi route trừ/healthvà trả401nếunull. Toàn bộ Worker nằm trong Khung và mã dùng chung.
Bước 2: upload an toàn#
-
Kiểm tra user có quyền upload, quota, request size, file type và tên file.
Đáp án
typescriptReadyQuota theo user: đếm
SELECT COUNT(*) FROM documents WHERE owner_id = ?trước khi nhận, hoặc dùng Durable Object theotenantIdở bản mở rộng. Tên file client gửi không được dùng làm key. -
Sinh object key ngẫu nhiên phía server, chẳng hạn
users/<authenticated-user-id>/<uuid>.Đáp án
typescriptReady -
Stream body vào private R2 bucket; không đọc toàn bộ file vào memory nếu file lớn.
Đáp án
typescriptReadyBucket giữ private; không bật public access cho bucket này.
-
Ghi metadata D1 với trạng thái
pending_scan, rồi enqueue message cójobId.Đáp án
typescriptReady -
Nếu bước giữa thất bại, ghi log có request ID và dùng cleanup/compensation để không để orphan vĩnh viễn.
Đáp án
typescriptReadyCompensation chỉ cứu lỗi bắt được. Process chết giữa hai bước vẫn để lại object mồ côi, nên cần job đối soát định kỳ (so danh sách key R2 với bảng
documents).
Bước 3: xử lý queue#
-
Consumer kiểm tra
jobIdđã được xử lý chưa bằng unique constraint/record idempotency.Đáp án
scan_log.job_idlà khoá chính;INSERT OR IGNOREkhông làm gì khi job lặp:typescriptReadydb.batch()chạy như một transaction. Việc cập nhật trạng thái cũng idempotent nhờ điều kiệnstatus = 'pending_scan'. -
Xác minh object tồn tại, chạy bước quét/trích metadata, cập nhật trạng thái
readyhoặcrejected.Đáp án
typescriptReady -
Lỗi tạm thời retry có giới hạn; lỗi dữ liệu không hợp lệ vào trạng thái thất bại/DLQ.
Đáp án
typescriptReadymax_retries: 3vàdead_letter_queueởwrangler.jsonc: hết 3 lần retry thì message vàoscan-upload-dlq. Lỗi dữ liệu vĩnh viễn (object không tồn tại) không ném lỗi mà ghirejectedrồiack, để không retry vô ích. -
Tạo một test gọi consumer hai lần với cùng job và chứng minh không có side effect trùng.
Đáp án
Vitest chạy trong runtime Workers (tham chiếu theo tài liệu Cloudflare, chưa chạy): gọi
scanOncehai lần với cùngjobId, khẳng địnhscan_logcó đúng một dòng. D1 trong test bắt đầu rỗng, nên cần một file setup áp migration, nếu không test báono such table. Cấu hình dưới đây theo trang cấu hình của tài liệu hiện hành (gói@cloudflare/vitest-plugin, hàmcloudflareTest); tên gói và cách importenvđã đổi giữa các phiên bản, chưa xác minh với bản bạn cài, nên đối chiếu tài liệu trước khi chép.vitest.config.ts:typescriptReadytest/apply-migrations.ts(khai báo thêm kiểuTEST_MIGRATIONSchoenvtrong test):typescriptReadyTest:
typescriptReady
Bước 4: download và cache#
-
Endpoint
GET /api/documents/:idtìm metadata bằngid + owner_idtrong cùng query.Đáp án
typescriptReadyTìm theo
idvàowner_idtrong cùng một query để không có khoảng hở kiểm tra rồi mới đọc. -
Chỉ tải R2 object nếu user là owner hoặc qua được policy chia sẻ.
Đáp án
Trong lời giải chỉ có owner (không có chia sẻ). Dòng
if (doc.status !== 'ready') return ... 409chặn tải file chưa quét xong;FILES.get(doc.object_key)chỉ chạy sau khi truy vấn ở trên trả hàng. Nếu thêm chia sẻ, đưa quyền vào bảng riêng và join trong cùng query. -
Response file đặt header an toàn; nội dung riêng tư trả
Cache-Control: private, no-store.Đáp án
typescriptReady -
Kiểm tra request của user B không thể tải file user A kể cả khi biết document ID/object key.
Đáp án
Hai token ở ma trận kiểm tra (xem yêu cầu 3 của Bước 5): user B gọi
GET /api/documents/$IDvới$IDcủa user A phải nhận404, giống hệt khi ID không tồn tại. Biết object key cũng vô dụng vì Worker không có route đọc R2 theo key.
Bước 5: staging, quan sát và dọn#
-
Tạo staging environment có D1/bucket/queue riêng; cấu hình lại các binding không tự kế thừa như mong đợi.
Đáp án
Thêm khối
"env": { "staging": { ... } }trongwrangler.jsonckhai báo lạid1_databases,r2_buckets,queuesvới D1, R2, queue (và DLQ) riêng, vì binding không kế thừa như mong đợi. Tạo tài nguyên (mỗi lệnh tác động tài khoản thật, chưa chạy):bashReady -
Thêm secret thử nghiệm qua Wrangler/dashboard; ví dụ
npx wrangler secret put API_KEY --env staging. Lệnh thêm secret triển khai Worker ngay, nên xác nhận environment/resource đích trước khi chạy; tuyệt đối không dùng secret production ở local hoặc staging.Đáp án
bashReadyLệnh này triển khai Worker ngay. Không dùng secret production ở local hoặc staging; ở local dùng
.dev.varsvới giá trị giả. -
Chạy migration local, request success/error, queue retry/DLQ, upload quá giới hạn và authorization matrix.
Đáp án
bashReadyMong đợi: xem bảng trong Khung và mã dùng chung. Thêm test queue retry/DLQ bằng cách làm
scanOnceném lỗi tạm thời trong môi trường thử. -
Deploy lên staging; gọi endpoint, xem Workers Logs, xác minh object/row/queue state và tắt cache cho dữ liệu user.
Đáp án
Sau khi kiểm chi phí và đích deploy:
npx wrangler deploy --env staging(hoặc qua CI). Gọi lại ma trận kiểm tra trên URL staging, xem Workers Logs (observabilityđã bật) và kiểm tra hàng trong D1, object trong R2, độ sâu queue. Phản hồi file đã cócache-control: private, no-store. -
Sau khi kiểm tra chi phí và đích deploy, mới deploy production theo CI/CD của dự án; dọn database, bucket, queue và secret lab khi xong.
Đáp án
Deploy production theo CI/CD của dự án sau khi staging sạch. Dọn lab (chỉ khi đã tạo thật):
bashReadyXoá thư mục
.wrangler/và.dev.varslocal. Với lab chỉ chạy--localthì không có gì trên tài khoản để dọn.
Lệnh Wrangler và cách khai báo binding có thể thay đổi. Tra Wrangler configuration, local development và tài liệu từng binding trước khi chạy thao tác remote. Đặc biệt, lệnh migration/bucket/deploy tác động resource thật nếu chọn remote/account production.
Khung và mã dùng chung
Tự làm trước, rồi mở. Chưa chạy trên Cloudflare thật (không dùng tài khoản hay API token Cloudflare, không wrangler deploy, không tạo tài nguyên có phí). Mức bằng chứng: code Worker đã qua tsc --noEmit với kiểu sinh bởi wrangler types (wrangler 4.x); chưa chạy wrangler dev, nên "Kết quả mong đợi" là suy ra từ code và tài liệu. Mọi lệnh --local chỉ dùng trạng thái giả lập trong thư mục .wrangler/; lệnh --remote, deploy, secret put, r2 bucket create tác động tài khoản thật nên chỉ chạy khi bạn đã chọn đúng tài khoản lab.
Sơ đồ
Worker đầy đủ (src/index.ts). Các yêu cầu ở trên dùng từng đoạn của file này.
Kết quả mong đợi (suy ra, chưa quan sát)
| Lệnh | Mong đợi |
|---|---|
/health | {"ok":true} |
| Upload không token | 401 |
Upload text/plain hợp lệ | 201 {"id":"<uuid>","status":"pending_scan"} |
GET ngay sau upload | Có thể 409 not ready vài trăm ms; sau khi consumer chạy: 200, body hello private, header cache-control: private, no-store, content-disposition: attachment |
GET bằng token user B | 404 (không lộ sự tồn tại; quy ước 404 cho tài nguyên của người khác) |
| Body 1 000 001 byte | 413 |
image/png | 415 |
| Test idempotency | Qua (n = 1) |
| Log local | Có scan_duplicate_skipped khi job lặp; không có token hay nội dung file trong log |
KV và Durable Object không dùng trong lời giải: đề cho phép bỏ khi use case không cần. Nếu muốn mở rộng quota theo tenant, dùng một Durable Object theo tenantId (không phải một object toàn hệ thống) để tăng bộ đếm tuần tự.
Lỗi hay gặp
- Quên
database_idthật khi deploy: lệnh local vẫn chạy, nhưng deploy trỏ sai hoặc không tìm thấy database. Chạywrangler d1 createrồi chép ID vào cấu hình. - Staging dùng nhầm binding production vì khối
env.stagingkhông khai báo lại binding. Đặt tên tài nguyên có hậu tố và kiểm bằng dashboard. wrangler devnối remote binding khi bạn bậtremotetrong binding: dữ liệu local và remote lẫn nhau. Mặc định giữ local.- Tin
content-typetừ client: nó chỉ lọc sơ. Dữ liệu thật vẫn nên kiểm magic bytes trong consumer. - Không có DLQ khai báo: message hết
max_retriesbị xoá, mất dấu job lỗi. - Gọi consumer với side effect ngoài (email, tính phí) mà không truyền idempotency key: lặp message sẽ gửi lặp.
putvào R2 với stream không biết độ dài: dùngFixedLengthStreamkhi biết kích thước (nêu theo tài liệu R2; chưa xác minh quy tắc bắt buộc trong mục E).- Ghi D1 và R2 không cùng transaction: nếu process chết giữa hai bước vẫn có object mồ côi; cần job đối soát định kỳ (compensation trong
catchchỉ cứu lỗi bắt được).
Tài liệu Cloudflare đã dùng: Wrangler configuration, R2 Workers API, D1 migrations và batch, Queues delivery guarantees, Queues DLQ, Wrangler environments, Vitest integration.
Definition of done#
- Có sơ đồ request flow và giải thích được lúc nào code chạy trên Worker, lúc nào asset được CDN trả trực tiếp.
- API xác thực user, validate input, phân quyền theo owner và không lộ bucket public.
- File stream vào R2; metadata/migration ở D1 hoặc DB được chọn có lý do.
- Queue consumer idempotent, có retry/DLQ và theo dõi message cũ.
- KV chỉ được dùng ở nơi chấp nhận stale; biết tại sao KV không làm database transaction.
- Có log để truy một request mà không lộ secret/PII.
- Staging dùng resource riêng; biết cách ước tính chi phí và dọn lab.
- Viết được một đoạn giải thích vì sao Cloudflare hoặc AWS phù hợp hơn với project đã chọn.
Liên hệ với repo Backend Guide#
Repo này đang minh họa một lựa chọn khác với Worker API thuần: VitePress build thành static site, deploy trên Cloudflare Pages và dùng R2 private cho ebook. Đọc wrangler.jsonc, .github/workflows/deploy.yml và phần Ebook trong README.md; lần theo build → Pages deployment → R2 binding → kiểm tra session Supabase → byte-range response. Dùng flow này để so sánh static hosting + Worker endpoint với app full-stack chạy trực tiếp trên Workers.