Deploy Website Tĩnh React/Vue Lên S3 + CloudFront: Chi Phí Gần Như Bằng 0
Danh Mục Bài Viết
- 1. Tại sao lại là S3 + CloudFront, không phải Vercel hay Netlify?
- 2. Kiến trúc tổng quan — hiểu trước, làm sau
- 3. Hướng dẫn từng bước — từ zero đến production
- 4. Tự động hóa deploy với GitHub Actions
- 5. Chi phí thực tế — tính cho bạn luôn
- 6. Lỗi thường gặp và cách fix
- 7. FAQ — những câu hỏi mình hay nhận được
- 8. Nghĩ mà xem — đây mới là cách deploy đúng cho frontend
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.
Tại sao lại là S3 + CloudFront, không phải Vercel hay Netlify?
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.
Kiến trúc tổng quan — hiểu trước, làm sau
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:
- S3 Bucket: Lưu toàn bộ file build (
dist/hoặcbuild/). Bucket không bật public access — chỉ CloudFront mới được đọc thông qua OAC (Origin Access Control). - CloudFront Distribution: Nhận request từ người dùng, kiểm tra cache, nếu miss thì lấy từ S3. Đây là điểm duy nhất expose ra internet.
- ACM Certificate: SSL/TLS miễn phí từ AWS Certificate Manager, gắn vào CloudFront để có HTTPS.
- Route 53 (tùy chọn): Nếu bạn có domain riêng, tạo Alias record trỏ về CloudFront. Nếu dùng domain mặc định
*.cloudfront.netthì không cần.
Luồng request: User → CloudFront (HTTPS, edge cache) → S3 (origin, private). Đơn giản, nhưng mạnh.
Hướng dẫn từng bước — từ zero đến production
1. Tạo S3 Bucket
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.
2. Upload file build lên S3
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.
3. Tạo CloudFront Distribution
Vào CloudFront → Create distribution. Phần Origin:
- Origin domain: Chọn S3 bucket vừa tạo (sẽ gợi ý tự động).
- Origin access: Chọn "Origin access control settings (recommended)" → Create new OAC. Đây là cách AWS khuyến nghị hiện tại, thay thế OAI cũ.
- Sau khi tạo distribution, AWS sẽ hiện một banner yêu cầu bạn cập nhật Bucket Policy. Copy đoạn policy đó, vào S3 bucket → Permissions → Bucket policy → Paste vào.
Phần Default cache behavior:
- Viewer protocol policy: "Redirect HTTP to HTTPS" — người dùng gõ HTTP vẫn được chuyển sang HTTPS tự động.
- Allowed HTTP methods: GET, HEAD là đủ cho website tĩnh.
Phần Settings:
- Default root object:
index.html— bắt buộc điền, nếu không CloudFront không biết trả file gì khi truy cập/. - Alternate domain names (CNAMEs): Nếu có domain riêng, điền vào đây (VD:
myapp.example.com). - Custom SSL certificate: Nếu có domain riêng → phải gắn certificate từ ACM (xem bước tiếp).
4. Cài HTTPS với ACM (miễn phí)
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.
5. Xử lý Client-Side Routing (React Router, Vue Router)
Đâ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:
- HTTP error code:
403→ Response page path:/index.html→ HTTP Response code:200 - Làm tương tự với
404
Bằ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.
6. Invalidation Cache khi deploy mới
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.
Tự động hóa deploy với GitHub Actions
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.
Chi phí thực tế — tính cho bạn luôn
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:
- S3 storage: ~0.023 USD/GB/tháng → 100 MB ≈ 0.002 USD
- CloudFront data transfer: ~0.085 USD/GB (khu vực Đông Nam Á) → 10 GB ≈ 0.85 USD
- Request: 0.0075 USD / 10.000 HTTPS request → 1 triệu request ≈ 0.75 USD
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.
Lỗi thường gặp và cách fix
"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.
FAQ — những câu hỏi mình hay nhận được
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.
Nghĩ mà xem — đây mới là cách deploy đúng cho frontend
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.
