# Thực hành: cho người dùng tải file lớn qua presigned URL và phục vụ nội dung tĩnh bằng CDN

> Mỗi gigabyte đi qua server ứng dụng tốn compute, bộ nhớ và băng thông mà bạn phải trả tiền, trong khi một chìa khóa tạm thời có thể làm thay việc đó.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-presigned-url-valet-key-cdn/

Thử hình dung khách hàng của bạn có 200 kỹ thuật viên hiện trường, mỗi người cuối ngày upload một video kiểm tra nặng 500 MB. Nếu mọi byte đều đi qua API, server ứng dụng phải nhận vào rồi đẩy ra khoảng 100 GB mỗi ngày, chỉ để làm một việc mà storage tự làm được.

Trang Azure Architecture Center của Microsoft mô tả đúng cái giá này: khi ứng dụng làm proxy cho upload và download, nó ngốn những tài nguyên quý như compute, bộ nhớ và băng thông. Pattern valet key đẩy phần truyền dữ liệu sang kho lưu trữ, còn ứng dụng chỉ giữ việc quyết định ai được làm gì.

Với một FDE, đây là yêu cầu gặp đi gặp lại: khách muốn người dùng của họ đẩy tài liệu, ảnh, log hay dữ liệu huấn luyện lên hệ thống bạn vừa triển khai. Hướng dẫn dưới đây đi từng bước để bạn dựng luồng đó trên laptop, rồi đặt CDN trước phần nội dung tĩnh.

## Bạn sẽ dựng gì, cần gì trước?

Bạn sẽ dựng ba thứ: một endpoint cấp URL upload có thời hạn, một luồng client upload thẳng lên storage, và một bước kiểm tra file sau khi upload xong. Phần cuối là storage cho nội dung tĩnh với CDN đứng phía trước.

Bạn cần một tài khoản cloud có object storage (S3 hoặc Azure Blob), một backend nhỏ mà bạn quen tay, và `curl`. Các đoạn code dưới đây là mã giả, đã giản lược để minh họa cấu trúc, không phải cú pháp SDK cụ thể; khi làm thật, bạn tra hàm tạo presigned URL trong SDK của mình.

## Bước 1: chiếc chìa khóa hẹp đến mức nào là đủ?

Theo tài liệu của AWS, một presigned URL được xác định bởi bốn thứ: bucket, object key, HTTP method và thời điểm hết hạn. Người cầm URL có thể upload đúng object đó mà không cần credential hay quyền AWS nào.

Microsoft định nghĩa valet key theo cùng tinh thần: token có thời hạn, chỉ áp dụng cho tài nguyên cụ thể, chỉ cho các thao tác định sẵn, chẳng hạn được ghi nhưng không được đọc. Ví dụ mẫu của họ giới hạn token trong một container và một file, thời hạn khoảng 3 phút, chỉ có quyền *create* blob, không được tải về, cập nhật hay xóa.

Quy tắc rút ra là mỗi chiều đều phải hẹp. Một file, một method, một khoảng thời gian, và không thừa quyền nào.

## Bước 2: viết endpoint cấp URL

Microsoft dùng một API nhẹ chạy trên Azure Function để cấp token. Bạn có thể làm tương tự bằng một route trong backend sẵn có:

```text
# Mã giả, giản lược để minh họa
POST /uploads            (người dùng đã đăng nhập)
kiểm tra người dùng có quyền upload cho tenant này
object_key = "uploads/{tenant_id}/{uuid_mới}"
url = presign(bucket="customer-uploads",
key=object_key,
method="PUT",
expires_in=)
lưu object_key vào DB với trạng thái "pending"
trả về { url, object_key }
```

Chi tiết quan trọng nhất là object key do server sinh, không bao giờ lấy từ tên file người dùng gửi lên. AWS lưu ý rằng upload bằng presigned URL vào một key đã tồn tại sẽ ghi đè object cũ; nếu client chọn được key, họ chọn được file của người khác để ghi đè.

Kiểm tra sau bước này: gọi endpoint hai lần, bạn phải nhận hai object key khác nhau.

## Bước 3: client upload thẳng, app đứng ngoài

Client lấy URL rồi đẩy file thẳng lên storage:

```bash
curl -X PUT --upload-file video.mp4 "$UPLOAD_URL"
```

Để kiểm tra phạm vi, dùng chính URL đó gọi GET bằng `curl "$UPLOAD_URL"`. Method là một phần định nghĩa của URL, nên yêu cầu này phải bị từ chối; nếu nó thành công, chìa khóa của bạn đang rộng hơn bạn nghĩ.

AWS gọi presigned URL là bearer token: ai cầm nó thì người đó có quyền. Vì vậy đừng ghi URL đầy đủ vào log ứng dụng, analytics hay ticket hỗ trợ.

**Điểm mấu chốt:** Ứng dụng nên cấp chìa khóa, còn việc chở dữ liệu thì để kho lưu trữ làm.

## Bước 4: thời hạn bao lâu thì đủ cho file lớn?

Microsoft khuyên đặt thời hạn ngắn để giảm rủi ro token bị lộ, nhưng cũng cảnh báo nếu quá ngắn thì upload lớn sẽ thất bại, nên cần cơ chế gia hạn. Con số 3 phút trong ví dụ mẫu hợp với ảnh nhỏ, không hợp với mọi trường hợp.

Thử làm một phép tính. Một file 1,8 GB, đường upload thực tế 10 Mbps: 1,8 × 1024 × 8 = 14.745,6 megabit, chia cho 10 ra khoảng 1.475 giây, tức gần 25 phút. Token 3 phút chắc chắn hỏng; cách hợp lý là tính theo file lớn nhất và đường truyền chậm nhất của khách, cộng thêm biên an toàn.

Về giới hạn trên, AWS cho phép đặt từ 1 phút đến 12 giờ trên console, còn với CLI hoặc SDK thì tối đa 7 ngày. Có một cái bẫy: nếu URL được ký bằng credential tạm thời, như session của IAM role, nó sẽ hết hạn cùng credential đó dù bạn đặt thời hạn dài hơn.

Download file lớn có một cái bẫy khác. Theo AWS, S3 kiểm tra thời hạn tại thời điểm request đến: lượt tải đang chạy vẫn tiếp tục khi URL hết hạn, nhưng nếu kết nối rớt và client tải tiếp sau mốc hết hạn thì sẽ thất bại. Vì vậy client nên biết xin URL mới từ endpoint của bạn thay vì thử lại mãi một URL cũ.

Đó là lý do bạn thêm một route gia hạn bên cạnh route ở bước 2. Route này không sinh key mới mà ký lại đúng key cũ, và chỉ cấp cho người đã sở hữu nó:

```text
# Mã giả, giản lược để minh họa
POST /uploads/{object_key}/renew   (người dùng đã đăng nhập)
tra object_key trong DB
từ chối nếu không thuộc người dùng này
từ chối nếu trạng thái khác "pending"
url = presign(bucket="customer-uploads",
key=object_key,
method="PUT",
expires_in=)
trả về { url }
```

Với download, làm tương tự: client gọi lại endpoint của bạn để nhận URL GET mới cho cùng object, sau khi server kiểm tra quyền đọc. Kiểm tra sau bước này: để một URL hết hạn, gọi PUT phải thất bại; gọi route renew rồi PUT bằng URL mới phải thành công, và object key trong DB không đổi.

## Bước 5: vì sao vẫn phải kiểm tra file sau upload?

Đây là phần nhiều người bỏ qua. Microsoft nói rõ: thường không thể tạo một key giới hạn dung lượng dữ liệu được ghi, cũng không giới hạn được số lần dùng key. Người dùng hợp lệ vẫn có thể đẩy lên file khổng lồ, file sai định dạng hoặc file độc hại.

Vì thế cần bước hậu kiểm: khi file về đến storage, một job đọc object key đang ở trạng thái "pending", kiểm tra kích thước, định dạng và quét nội dung độc hại, rồi mới chuyển sang "ready". Route renew ở bước 4 cũng dựa vào trạng thái này: file đã "ready" thì không còn được cấp URL ghi nữa.

Microsoft cũng khuyên theo dõi chi phí để phát hiện dung lượng tăng bất thường. Trên Azure, quyền *create* khiến token gần như chỉ dùng được một lần, giúp thu hẹp thiệt hại nếu bị lộ.

## Lỗi hay gặp và cách sửa

| Triệu chứng | Nguyên nhân thường gặp | Cách sửa |
|---|---|---|
| `SignatureDoesNotMatch` | Đồng hồ máy ký URL lệch giờ | Đồng bộ với NTP server; lệch nhỏ cũng đủ làm chữ ký hỏng |
| `SignatureDoesNotMatch` dù giờ đúng | Proxy ở giữa sửa header hoặc query string | Cho request đi thẳng tới storage, không qua proxy viết lại URL |
| File lớn upload hỏng giữa chừng | Thời hạn quá ngắn so với kích thước file | Tính theo file lớn nhất, dùng route renew ở bước 4 |
| URL hết hạn sớm hơn đã đặt | Ký bằng credential tạm thời | Kiểm tra thời hạn session của role ký URL |

Nếu khách cần đảm bảo file không bị hỏng trên đường truyền, AWS hỗ trợ kiểm tra tính toàn vẹn của upload bằng checksum trong SigV4.

## Bước 6: nội dung tĩnh thì sao, có cần chìa khóa không?

Không phải file nào cũng cần valet key. Pattern Static Content Hosting của Microsoft đặt ảnh, script, stylesheet thẳng trên storage và thêm CDN phía trước để tăng hiệu năng, đổi lại là thêm chi phí. Container phải cho đọc công khai nhưng tuyệt đối không cho ghi công khai; nội dung cần bảo vệ thì quay về dùng valet key.

CDN, theo Wikipedia, là mạng proxy server và data center phân tán theo địa lý, đưa nội dung đến gần người dùng hơn. Tài liệu system-design-primer phân biệt hai kiểu: pull CDN lấy nội dung từ server gốc khi người dùng đầu tiên yêu cầu, nên lượt đầu chậm hơn; push CDN hợp với nội dung ít truy cập hoặc hiếm khi cập nhật.

Cái giá của CDN, cũng theo system-design-primer, là chi phí, nội dung có thể cũ nếu bạn cập nhật trước khi TTL hết, và phải đổi URL của nội dung tĩnh. Cách xử lý thực tế là gắn phiên bản vào tên file, ví dụ `app.v42.js`: mỗi lần deploy là một URL mới, nên không còn bản cũ nào nằm chờ TTL.

## Bước 7: tận mắt thấy CDN trả bản cũ, rồi xử lý

Bài tập này giản lược, dùng một pull CDN trỏ vào container tĩnh vừa tạo. Trước hết kiểm tra quyền: thử ghi thẳng một file vào container mà không có token nào, request đó phải bị từ chối, giống như phép thử GET ở bước 3.

```bash
# $CDN là domain CDN trỏ vào container tĩnh (giản lược)
curl "$CDN/app.js"   # lần 1: CDN phải về gốc lấy file
curl "$CDN/app.js"   # lần 2: nên trả từ cache, nhanh hơn
```

Giờ sửa nội dung `app.js` và upload đè cùng tên lên storage, rồi gọi lại qua CDN. Nếu TTL chưa hết, nhiều khả năng bạn vẫn nhận bản cũ: đó chính là nội dung stale mà system-design-primer cảnh báo.

Cuối cùng upload bản mới dưới tên `app.v2.js` và gọi URL đó. Bạn sẽ nhận nội dung mới ngay, vì với CDN đây là một file chưa từng thấy; lượt đầu có thể chậm hơn đúng như mô tả về pull CDN. Kiểm tra đạt khi bạn giải thích được cả ba kết quả.

## Ở site khách, kỹ năng này trông như thế nào?

Ở site khách, câu hỏi đầu tiên thường không phải dùng S3 hay Blob mà là ai đang làm proxy cho file. Hãy mở log và biểu đồ băng thông của server ứng dụng: nếu traffic upload chiếm phần lớn, bạn có một đề xuất cụ thể và đo được.

Khi đọc JD cho vị trí FDE, bạn có thể để ý những mô tả về việc nhận file hay dữ liệu từ khách, chẳng hạn các cụm như "file ingestion" hoặc "data onboarding"; nếu có, đó là dịp để kể lại đúng bài thực hành này khi phỏng vấn.

Trong CV, đừng chỉ viết "dùng S3"; hãy viết bạn đã chuyển upload sang presigned URL, chọn thời hạn token theo kích thước file thật, có route gia hạn và thêm bước hậu kiểm nội dung.

Việc đầu tiên nên làm trong tuần đầu ở khách: tìm endpoint upload nặng nhất, đo xem bao nhiêu byte đang đi qua app mỗi ngày, rồi mang con số đó cùng bản thiết kế presigned URL ở trên vào buổi họp kỹ thuật kế tiếp.

**Thử ngay tuần này:**

- Viết một endpoint nhỏ cấp presigned URL cho phương thức PUT, object key do server sinh, rồi dùng curl để upload và thử gọi GET bằng chính URL đó.
- Tính thời gian upload file lớn nhất mà khách thực sự có ở băng thông thấp nhất của họ, rồi đặt thời hạn token theo con số đó thay vì đoán.
- Rà lại các container hoặc bucket đang phục vụ ảnh, JS, CSS trong dự án hiện tại: xác nhận không có cái nào bật ghi công khai.

## Nguồn

- [Valet Key pattern - Azure Architecture Center | Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/patterns/valet-key)

- [Download and upload objects with presigned URLs - Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)

- [Static Content Hosting pattern - Azure Architecture Center | Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/patterns/static-content-hosting)

- [system-design-primer: Content delivery network](https://github.com/donnemartin/system-design-primer)

- [Content delivery network - Wikipedia](https://en.wikipedia.org/wiki/Content_delivery_network)
