Ngôn ngữ hiển thị:
Một trong những câu hỏi mình hay nhận được từ các bạn mới học AWS là: "Anh ơi, em có cái web React/Vue muốn deploy lên mà không muốn tốn nhiều tiền hosting, làm thế nào?" Câu trả lời gần như luôn là: S3 + CloudFront. Chi phí thực tế cho một website tĩnh trung bình chỉ khoảng 0–2 USD/tháng, đôi khi còn nằm gọn trong Free Tier. Nói thật thì đây là một trong những bộ combo AWS "đáng đồng tiền" nhất mà mình biết.
Vercel và Netlify đều tuyệt, đặc biệt với team nhỏ hoặc side project. Nhưng khi bạn đang dùng AWS làm hạ tầng chính — database trên RDS, backend trên EC2 hay Lambda — thì giữ frontend cùng hệ sinh thái AWS có lợi hơn nhiều: billing tập trung, IAM kiểm soát truy cập đồng bộ, và đặc biệt là không có vendor lock-in với bên thứ ba.
S3 (Simple Storage Service) ở đây đóng vai trò lưu trữ các file tĩnh: HTML, JS, CSS, ảnh. CloudFront là CDN (Content Delivery Network) toàn cầu của AWS — nó cache nội dung tại các edge location gần người dùng nhất, giúp tốc độ load cực nhanh dù người truy cập ở Hà Nội, Tokyo hay London.
Khổ nỗi là nhiều bạn cứ nghĩ S3 static hosting là "mở thẳng bucket ra internet", rồi gặp đủ thứ vấn đề: không có HTTPS, URL xấu, chi phí transfer ra ngoài cao, không cache được. Làm đúng cách — dùng CloudFront làm lớp trước — thì tất cả những vấn đề đó biến mất.
Trước khi vào console AWS bấm bấm, mình muốn bạn hình dung rõ luồng hoạt động:
dist/ hoặc build/). Bucket không bật public access — chỉ CloudFront mới được đọc thông qua OAC (Origin Access Control).*.cloudfront.net thì không cần.Luồng request: User → CloudFront (HTTPS, edge cache) → S3 (origin, private). Đơn giản, nhưng mạnh.
Vào AWS Console → S3 → Create bucket. Đặt tên bucket (nên đặt trùng domain nếu có, VD: myapp.example.com). Region chọn tùy ý — thường chọn us-east-1 để phối hợp tốt với CloudFront và ACM.
Quan trọng: Phần "Block Public Access" — để nguyên tất cả checkbox "Block all public access" đều tích. Đừng mở public. Bucket phải private, CloudFront sẽ truy cập qua OAC.
Sau khi tạo xong, vào bucket → Properties → Static website hosting: KHÔNG bật. Tính năng này chỉ cần khi bạn serve trực tiếp từ S3 URL, còn đã có CloudFront thì không cần và cũng không nên bật.
Build project của bạn trước:
# React (Create React App)
npm run build
# Vue (Vite)
npm run build
# Kết quả thường nằm ở thư mục dist/ hoặc build/Sau đó upload lên S3. Cách nhanh nhất là dùng AWS CLI:
aws s3 sync ./dist s3://your-bucket-name --deleteFlag --delete xóa các file trên S3 không còn tồn tại ở local — rất quan trọng để tránh rác cũ trên production. Nếu chưa cài AWS CLI, xem hướng dẫn tại tài liệu chính thức của AWS.
Vào CloudFront → Create distribution. Phần Origin:
Phần Default cache behavior:
Phần Settings:
index.html — bắt buộc điền, nếu không CloudFront không biết trả file gì khi truy cập /.myapp.example.com).Lưu ý quan trọng: Certificate dùng cho CloudFront bắt buộc phải tạo ở region us-east-1 (N. Virginia), dù CloudFront là global service. Đây là điểm nhiều bạn hay nhầm, mất công tạo certificate ở region khác rồi không gắn được.
Vào ACM (chắc chắn đang ở us-east-1) → Request certificate → Chọn "Public certificate" → Điền domain name → Chọn DNS validation → Request.
AWS sẽ cung cấp một CNAME record để verify bạn sở hữu domain. Nếu domain dùng Route 53, bấm "Create records in Route 53" là xong tự động. Nếu domain ở Cloudflare hay GoDaddy, thêm CNAME record thủ công vào DNS của họ. Thường mất 5–30 phút để xác thực xong.
Đây là bước hay bị bỏ qua nhất và gây bug khó chịu nhất. Nếu app của bạn dùng React Router hoặc Vue Router (history mode), khi người dùng truy cập thẳng vào một URL như /about hay /product/123, CloudFront sẽ tìm file about hay product/123 trong S3 — không tìm thấy → trả về 403 hoặc 404 xấu xí.
Fix rất đơn giản: Vào CloudFront distribution → Error pages → Create custom error response:
403 → Response page path: /index.html → HTTP Response code: 200404Bằng cách này, mọi route không tìm thấy file thực sự đều trả về index.html với HTTP 200, để React Router/Vue Router tự xử lý routing ở client-side.
Mẹo từ kinh nghiệm: Khi deploy lên CloudFront lần đầu và thấy web hoạt động bình thường ở trang chủ, đừng vội mừng — thử F5 ở một sub-route khác. Nếu ra 403/404 thì bạn cần làm bước custom error response này.
CloudFront cache file theo TTL. Mặc định TTL có thể lên đến 24h, nghĩa là bạn deploy code mới lên S3 nhưng người dùng vẫn thấy code cũ. Fix bằng cách invalidate cache sau mỗi lần deploy:
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*"Lệnh /* invalidate toàn bộ cache. Mỗi tháng bạn được 1000 invalidation path miễn phí, sau đó tính phí nhỏ. Với website thay đổi thường xuyên, chiến lược tốt hơn là dùng content hashing trong tên file (Vite và CRA đều làm tự động: main.abc123.js) — khi đó CloudFront cache miss tự nhiên cho file mới, không cần invalidate.
Làm tay mãi cũng mệt. Mình thường setup GitHub Actions để tự động build + deploy mỗi khi push lên main:
name: Deploy to S3 + CloudFront
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install & Build
run: |
npm ci
npm run build
- name: Deploy to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-southeast-1
run: |
aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} --delete
- name: Invalidate CloudFront
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"Nhớ thêm 4 secrets vào GitHub repo: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET, CLOUDFRONT_DISTRIBUTION_ID. IAM user cho CI chỉ cần permission s3:PutObject, s3:DeleteObject, s3:ListBucket và cloudfront:CreateInvalidation — không cần full admin.
Nói "gần như bằng 0" thì phải có số liệu cụ thể. AWS Free Tier 12 tháng đầu cho S3 gồm 5 GB lưu trữ và 20.000 GET request/tháng. CloudFront Free Tier vĩnh viễn là 1 TB data transfer và 10 triệu HTTP request/tháng.
Với một website tĩnh thông thường (dưới 500 MB, traffic vài trăm nghìn pageview/tháng), chi phí sau Free Tier vào khoảng:
Tổng cộng cho một site traffic vừa phải: dưới 2 USD/tháng. So sánh với Vercel Pro ($20/tháng) hay hosting shared VPS cơ bản ($5–10/tháng), thì S3 + CloudFront thắng rõ ràng về chi phí thuần.
"Access Denied" khi truy cập web qua CloudFront: 90% là do Bucket Policy chưa được cập nhật sau khi tạo OAC. Vào CloudFront → Origins → Edit → Copy lại policy → Paste vào S3 Bucket Policy. Nếu vẫn lỗi, kiểm tra lại OAC đã được assign đúng vào distribution chưa.
Web load được nhưng F5 ở sub-route ra 403: Chưa làm bước Custom Error Response (403 → index.html). Xem lại Bước 5 ở trên.
Deploy code mới nhưng web vẫn hiển thị code cũ: Cache chưa được invalidate. Chạy lệnh aws cloudfront create-invalidation --distribution-id ID --paths "/*" hoặc vào Console → CloudFront → Invalidations → Create.
Certificate không hiện trong danh sách khi chọn cho CloudFront: Certificate không tạo ở region us-east-1. Tạo lại ở đúng region đó.
URL mặc định của CloudFront xấu (d1abc123.cloudfront.net): Cần gắn domain riêng qua Route 53 hoặc DNS provider khác. Điền CNAME/Alternate domain vào CloudFront và gắn SSL certificate tương ứng.
Q: S3 + CloudFront có handle được app React dùng API calls không, hay chỉ cho web tĩnh hoàn toàn?A: Handle được hoàn toàn. S3 + CloudFront chỉ serve phần frontend (HTML/JS/CSS). Còn API calls từ React/Vue vẫn gọi sang backend riêng (EC2, Lambda, hay bất kỳ server nào). Hai thứ hoàn toàn độc lập — đây chính là mô hình Jamstack phổ biến nhất hiện tại.
Q: Có nên bật Static website hosting trong S3 không?A: Không cần và không nên khi đã dùng CloudFront. Static website hosting của S3 tạo ra một HTTP endpoint public — vừa không có HTTPS, vừa expose trực tiếp S3 ra internet. Với OAC + CloudFront, S3 giữ private hoàn toàn là đúng chuẩn bảo mật.
Q: Muốn giới hạn truy cập web chỉ cho một số IP cụ thể thì làm thế nào?A: Dùng CloudFront Geo Restriction (chặn theo quốc gia) hoặc WAF (Web Application Firewall) với IP whitelist/blacklist rule. WAF có phí riêng (~5 USD/tháng cho Web ACL), nhưng nếu chỉ cần filter IP đơn giản thì Lambda@Edge cũng là option rẻ hơn.
Q: Chi phí tăng đột ngột nếu bị DDoS thì sao?A: Đây là rủi ro thực sự với mô hình pay-per-use. CloudFront có tích hợp AWS Shield Standard miễn phí, bảo vệ cơ bản khỏi DDoS layer 3/4. Nếu lo lắng hơn, bật AWS WAF với rate limiting rule để chặn request bất thường. AWS cũng có chính sách hỗ trợ credit cho DDoS attack trong một số trường hợp — tham khảo AWS Shield.
S3 + CloudFront không phải giải pháp "rẻ mà tệ". Đây thực sự là cách Netflix, Airbnb, và hàng nghìn công ty lớn serve frontend của họ — chỉ là ở quy mô lớn hơn nhiều. Bạn đang dùng đúng nền tảng đó, với chi phí micro vì traffic còn nhỏ.
Điều mình muốn nhấn mạnh là: setup ban đầu mất khoảng 30–60 phút, nhưng sau đó bạn gần như không cần đụng vào nữa. Kết hợp với GitHub Actions để tự động hóa deploy, toàn bộ workflow chỉ còn là: git push → web tự cập nhật trong 2–3 phút.
Bài tiếp theo mình sẽ đi vào kiến trúc AWS đầy đủ hơn cho một ứng dụng web production quy mô vừa — kết hợp frontend trên S3/CloudFront, backend trên EC2 hoặc ECS, database trên RDS, cùng với load balancer và auto scaling. Nếu bạn đang chuẩn bị ra mắt sản phẩm thực sự, bài đó sẽ hữu ích.