Bối Cảnh Và Mục Tiêu
Trước đây mình deploy portfolio trên Vercel vì đây gần như là lựa chọn mặc định cho một dự án Next.js: setup nhanh, build đơn giản và gần như không phải bận tâm nhiều đến hạ tầng. Với một portfolio cá nhân, cách đó hoàn toàn hợp lý.
Tuy nhiên, sau một thời gian, mình muốn đi xa hơn câu chuyện deploy cho chạy được. Mình muốn hiểu rõ hơn cách một website tĩnh có thể được đưa lên production trên AWS, cách DNS được cấu hình, certificate được cấp như thế nào, CI/CD vận hành ra sao, và quan trọng hơn là phải xử lý thế nào khi kiến trúc ban đầu không thể triển khai đúng kế hoạch.
Bài viết này ghi lại toàn bộ quá trình migrate portfolio từ Vercel sang AWS, bắt đầu từ phương án S3 + CloudFront + Route 53 + ACM, sau đó chuyển sang EC2 + Nginx + Route 53 + Let's Encrypt khi gặp giới hạn ở cấp account AWS.
Đây không chỉ là một lần đổi nơi host website. Nó là một bài thực hành khá đầy đủ về hạ tầng, DNS, TLS, CI/CD, Terraform và cách ra quyết định khi môi trường thực tế không đi đúng như thiết kế ban đầu.
AWS vs Vercel
Nếu chỉ nhìn từ góc độ ship nhanh một portfolio, Vercel gần như luôn dễ chịu hơn. Nhưng khi mình coi đây là một bài thực hành hạ tầng thật, AWS lại có lợi thế rõ hơn ở khả năng kiểm soát, mở rộng và giá trị học được sau mỗi bước triển khai.
Nhìn ngắn gọn, khác biệt giữa hai bên có thể tóm lại như sau.
| Tiêu chí | Vercel | AWS |
|---|---|---|
| Deploy | Rất nhanh | Phải setup |
| Chi phí nhỏ | Rẻ hoặc free | Có thể cao hơn |
| Chi phí khi scale | Đắt hơn | Rẻ hơn |
| Developer experience | Cực tốt | Thấp hơn |
| Control | Thấp | Full |
| Scaling | Auto | Phải tự làm |
| Infra | Không cần hiểu sâu | Phải hiểu và tự quản |
| Production lớn | Không tối ưu bằng | Tốt hơn |
Kiến Trúc Ban Đầu Được Chọn
Phương án đầu tiên mình chọn là kiến trúc static khá chuẩn trên AWS: S3 để lưu static files, CloudFront để làm CDN, Route 53 để quản lý DNS, ACM để cấp SSL miễn phí, Terraform để quản lý hạ tầng bằng code, và GitHub Actions để build và deploy.
Trước khi triển khai, mình kiểm tra lại project và xác nhận rằng app có thể static export được. Đây là điều kiện quan trọng, vì nếu site có thể build hoàn toàn ra static output thì S3 + CloudFront gần như là phương án phù hợp nhất trên lý thuyết: gọn, rẻ, dễ scale và ít phải vận hành server.
Nếu mọi thứ đi đúng kế hoạch, kiến trúc mong muốn sẽ là: User -> Route 53 -> CloudFront -> S3.

Sự Cố Khi Triển Khai CloudFront
Vấn đề xuất hiện ở đúng bước quan trọng nhất: tạo CloudFront distribution.
AWS trả về lỗi cho biết account của mình phải được verify trước khi có thể tạo CloudFront resource mới. Điều này có nghĩa là ứng dụng không sai, Terraform không sai, DNS và certificate flow cũng không sai, nhưng account AWS chưa đủ điều kiện để đi tiếp với CloudFront.
Đây là một tình huống rất thực tế production. Nhiều khi kiến trúc đúng và code đúng vẫn chưa đủ. Hệ thống vẫn có thể bị chặn bởi giới hạn ở cấp account hoặc dịch vụ.

Cách Mình Xử Lý Vấn Đề
Mình xử lý sự cố CloudFront theo hai hướng.
Hướng thứ nhất là mở support case với AWS để xác nhận đây là giới hạn ở cấp account, không phải do mình cấu hình sai. Việc này quan trọng vì nó giúp phân biệt rõ đâu là lỗi hạ tầng thật sự, đâu là lỗi do implementation.
Hướng thứ hai là thực dụng hơn: thay vì chờ CloudFront được mở, mình chuyển sang một phương án khác để đưa site lên production ngay.
Đây là quyết định quan trọng nhất trong cả bài thực hành. Nếu cứ cố giữ kiến trúc ban đầu bằng mọi giá, project sẽ bị đứng lại ở đúng điểm cần ship. Thay vào đó, mình chọn một kiến trúc đơn giản hơn nhưng vận hành được ngay.
- 1Vào AWS Support Center và tạo một support case mới.
- 2Chọn nhóm Account and billing, vì đây là nhóm hỗ trợ miễn phí và phù hợp với các vấn đề liên quan đến trạng thái account.
- 3Mô tả rõ lỗi khi tạo CloudFront distribution, bao gồm nguyên văn error message AWS trả về.
- 4Nếu màn hình hoặc log có request ID thì đính kèm luôn trong case để support có thêm dữ liệu tra cứu.
- 5Ghi rõ mục tiêu triển khai: một portfolio cá nhân trên AWS đang bị chặn ở bước tạo CloudFront resource.
- 6Sau khi gửi case, dùng phản hồi đó để xác nhận đây là giới hạn ở cấp account chứ không phải do Terraform hay cấu hình ứng dụng.

Phương Án Thay Thế: EC2 + Nginx
Kế hoạch mới của mình là: EC2, Nginx, Route 53, Let's Encrypt và GitHub Actions.
Ý tưởng rất đơn giản: build Next.js ra static files, đưa static files lên EC2, dùng Nginx phục vụ nội dung, dùng Let's Encrypt để cấp HTTPS, dùng Route 53 để quản lý domain, và dùng GitHub Actions để deploy tự động.
So với phương án S3 + CloudFront, đây không phải kiến trúc đẹp bằng trên giấy. Nhưng đổi lại, nó thực tế, dễ hiểu và quan trọng nhất là chạy được ngay trong điều kiện account hiện tại.
Tổng Quan Kiến Trúc Production Hiện Tại
Kiến trúc production mà portfolio đang dùng hiện nay là: User -> Route 53 -> EC2 -> Nginx -> static files.
Phần CI/CD hoạt động theo luồng: GitHub -> GitHub Actions -> SSH/rsync -> EC2.
Còn HTTPS được xử lý theo hướng: Let's Encrypt / Certbot -> Nginx on EC2.
Đây là một hệ thống nhỏ, đủ rõ ràng để kiểm soát, nhưng vẫn chạm vào đầy đủ những phần cốt lõi của một môi trường production thật.

Phân Tích Chi Tiết Từng Thành Phần
Route 53 được dùng để quản lý DNS cho letrungtruc.dev và www.letrungtruc.dev. Sau khi chuyển nameserver sang Route 53, toàn bộ phần DNS được quản lý ngay trong AWS. Điều này giúp hạ tầng đồng nhất hơn và thuận tiện khi làm việc với Terraform.
Website hiện chạy trên một instance t3.micro trong region ap-southeast-1. Mình chọn t3.micro vì đủ cho một static portfolio, chi phí thấp và phù hợp hơn với điều kiện account tại thời điểm triển khai. EC2 được gắn Elastic IP để địa chỉ public không thay đổi.
Mình không tạo custom VPC mới. Hệ thống dùng default VPC và public subnet có sẵn trong account. Với một portfolio cá nhân, đây là lựa chọn hợp lý hơn việc dựng một network topology quá phức tạp.
Nginx là lớp web server phục vụ static files. Sau khi build, nội dung site được deploy vào /var/www/portfolio. Nginx trỏ root vào thư mục này và serve toàn bộ nội dung cho người dùng.
Vì production path hiện tại không dùng CloudFront hay ALB, mình không dùng ACM cho runtime hiện tại. Thay vào đó, mình dùng Let's Encrypt và certbot. Certbot xin certificate trực tiếp cho cả letrungtruc.dev và www.letrungtruc.dev.
GitHub Actions hiện phụ trách checkout source, setup Node.js, npm ci, npm run build, copy thư mục out/ lên EC2 và reload Nginx. Flow này rất đơn giản, nhưng với một portfolio cá nhân thì đơn giản thường là lợi thế.
Security group hiện mở 80 cho HTTP, 443 cho HTTPS và 22 cho SSH. Trong quá trình triển khai, mình đã phải cân bằng giữa bảo mật và tính thực dụng. Khi cần GitHub Actions SSH vào EC2 để deploy, việc mở lại SSH cho workflow là điều cần thiết để pipeline hoạt động ổn định.
Toàn Bộ Quy Trình Thực Hành
Nếu nhìn lại toàn bộ quá trình, flow thực hành của mình diễn ra theo các bước sau.
1. Kiểm tra xem project có thể static export hay không.
2. Chuyển cấu hình Next.js sang output static.
3. Dựng hạ tầng plan đầu bằng Terraform.
4. Gặp lỗi khi tạo CloudFront distribution.
5. Mở support case để xác nhận vấn đề ở cấp account.
6. Thiết kế lại plan theo hướng EC2 + Nginx.
7. Tạo EC2, Elastic IP, security group và Route 53 records.
8. Cài Nginx và cấu hình thư mục phục vụ static site.
9. Bật HTTPS bằng certbot.
10. Thay workflow deploy từ flow cũ sang GitHub Actions -> SSH/rsync -> EC2.
11. Test production và cleanup phần hạ tầng cũ không còn dùng.
Điều mình thấy hay nhất ở quá trình này là mọi thứ không đi theo đúng một đường thẳng. Kiến trúc ban đầu hợp lý, nhưng điều kiện thực tế buộc mình phải thay đổi. Và chính chỗ đó mới phản ánh đúng tinh thần của một bài thực hành production.
Kết Luận Và Bài Học Rút Ra
Sau cùng, mình không coi bài này đơn thuần là một lần move hosting từ Vercel sang AWS. Nó giống một case study nhỏ về cách một decision kỹ thuật phải thay đổi khi đi vào môi trường thực tế.
Trên lý thuyết, S3 + CloudFront + Route 53 + ACM vẫn là kiến trúc static mình đánh giá cao hơn cho use case này. Nhưng trong bối cảnh account chưa được mở CloudFront, lựa chọn đúng không phải là tiếp tục cố chấp với thiết kế ban đầu, mà là chuyển sang một phương án triển khai được, vận hành được và ship được.
Với mình, đó là bài học đáng giá nhất của lần migrate này: trong production, kiến trúc tốt nhất không phải lúc nào cũng là kiến trúc đẹp nhất, mà là kiến trúc giải quyết được bài toán trong điều kiện thực tế.