cat blog/.md
Deploy Nuxt.js + FastAPI lên AWS ECS (Phần 2) — Route 53, CloudFront, SSL và WAF
Ở Phần 1, chúng ta đã có containers chạy trên ECS Fargate, CI/CD tự động qua GitHub Actions, ALB phân phối traffic. Nhưng users vẫn đang truy cập qua URL kiểu myapp-alb-123456.ap-southeast-1.elb.amazonaws.com — không HTTPS, không CDN, không firewall.
Phần 2 này sẽ biến hệ thống thành production-ready thực sự: domain riêng, HTTPS miễn phí, CDN toàn cầu, tường lửa chống tấn công, auto-scaling, và monitoring.
Kiến trúc hoàn chỉnh
Sau khi hoàn thành Phần 2, hệ thống sẽ trông như thế này:
User → yourdomain.com
↓
Route 53 (DNS)
↓
CloudFront (CDN + HTTPS)
↓
WAF (Web Application Firewall)
↓
ALB (Application Load Balancer)
↙ ↘
Nuxt.js FastAPI
(ECS) (ECS)
↓
RDS / Secrets Manager
Mỗi layer thêm một lớp giá trị: Route 53 quản lý DNS, CloudFront cache static assets và terminate SSL, WAF chặn traffic xấu trước khi tới app, ALB route request đến đúng container.
Bước 1: ACM — SSL Certificate miễn phí
AWS Certificate Manager cung cấp SSL certificate miễn phí, tự động renew. Nhưng có một điểm quan trọng: nếu dùng với CloudFront, certificate phải tạo ở region us-east-1 (N. Virginia), bất kể ECS chạy ở region nào.
# QUAN TRỌNG: Phải dùng region us-east-1 cho CloudFront
aws acm request-certificate \
--domain-name yourdomain.com \
--subject-alternative-names "*.yourdomain.com" \
--validation-method DNS \
--region us-east-1
Wildcard *.yourdomain.com cho phép dùng chung certificate cho www.yourdomain.com, api.yourdomain.com, staging.yourdomain.com… — tiện hơn nhiều so với tạo certificate riêng cho mỗi subdomain.
Sau khi request, AWS yêu cầu validate quyền sở hữu domain bằng DNS record:
# Lấy CNAME record cần thêm vào DNS
aws acm describe-certificate \
--certificate-arn arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxx \
--region us-east-1 \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
# Kết quả:
# {
# "Name": "_abc123.yourdomain.com",
# "Type": "CNAME",
# "Value": "_def456.acm-validations.aws"
# }
Thêm CNAME record này vào DNS provider (hoặc Route 53 nếu đã chuyển). Sau 5-10 phút, certificate sẽ chuyển sang trạng thái ISSUED.
Tạo luôn một certificate ở region của ALB (VD: ap-southeast-1) cho ALB HTTPS listener:
aws acm request-certificate \
--domain-name yourdomain.com \
--subject-alternative-names "*.yourdomain.com" \
--validation-method DNS \
--region ap-southeast-1
Bước 2: Route 53 — DNS Management
Tạo Hosted Zone
aws route53 create-hosted-zone \
--name yourdomain.com \
--caller-reference "$(date +%s)"
Sau khi tạo, Route 53 trả về 4 name servers. Bạn cần cập nhật name servers ở nơi mua domain (Namecheap, GoDaddy, Google Domains…) để trỏ về Route 53:
# Lấy name servers của hosted zone
aws route53 get-hosted-zone \
--id /hostedzone/ZONE_ID \
--query 'DelegationSet.NameServers'
# Kết quả VD:
# [
# "ns-123.awsdns-45.com",
# "ns-678.awsdns-90.net",
# "ns-111.awsdns-22.org",
# "ns-333.awsdns-44.co.uk"
# ]
Copy 4 name servers này vào domain registrar. DNS propagation mất 24-48 giờ, nhưng thực tế thường xong trong 1-2 giờ.
Tại sao dùng Route 53 thay vì DNS ở registrar?
- Alias records: Route 53 hỗ trợ alias record trỏ thẳng tới ALB, CloudFront — không cần CNAME (nhanh hơn, hoạt động với root domain)
- Health checks: Route 53 tự kiểm tra endpoint và failover khi cần
- Latency-based routing: Route users tới region gần nhất
- Tích hợp native: Validate ACM certificate chỉ cần 1 click trong console
Bước 3: ALB HTTPS Listener
Trước khi thêm CloudFront, hãy bật HTTPS trên ALB trước. Tạo HTTPS listener (port 443) và redirect HTTP sang HTTPS:
# Tạo HTTPS listener trên ALB
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:ap-southeast-1:ACCOUNT_ID:loadbalancer/app/myapp-alb/xxx \
--protocol HTTPS \
--port 443 \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--certificates CertificateArn=arn:aws:acm:ap-southeast-1:ACCOUNT_ID:certificate/yyy \
--default-actions '[{
"Type": "forward",
"TargetGroupArn": "arn:aws:elasticloadbalancing:...:targetgroup/frontend-tg/xxx"
}]'
# Redirect HTTP → HTTPS
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:ap-southeast-1:ACCOUNT_ID:loadbalancer/app/myapp-alb/xxx \
--protocol HTTP \
--port 80 \
--default-actions '[{
"Type": "redirect",
"RedirectConfig": {
"Protocol": "HTTPS",
"Port": "443",
"StatusCode": "HTTP_301"
}
}]'
SSL policy ELBSecurityPolicy-TLS13-1-2-2021-06 chỉ cho phép TLS 1.2 và 1.3 — disable các protocol cũ (TLS 1.0, 1.1) để đảm bảo bảo mật.
Thêm routing rule cho /api/* trên HTTPS listener:
aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:...:listener/xxx \
--priority 10 \
--conditions '[{"Field":"path-pattern","Values":["/api/*"]}]' \
--actions '[{"Type":"forward","TargetGroupArn":"arn:...backend-tg..."}]'
Bước 4: CloudFront — CDN toàn cầu
CloudFront đặt content gần users hơn thông qua edge locations trên toàn thế giới. Với Nuxt.js SSR, CloudFront cache static assets (JS, CSS, images) ở edge, giảm load cho ECS containers.
Tạo CloudFront Distribution
aws cloudfront create-distribution \
--distribution-config '{
"CallerReference": "myapp-2026",
"Comment": "MyApp Production",
"Enabled": true,
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "alb-origin",
"DomainName": "myapp-alb-xxx.ap-southeast-1.elb.amazonaws.com",
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {
"Quantity": 1,
"Items": ["TLSv1.2"]
}
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "alb-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 7,
"Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"]
},
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
"Compress": true
},
"Aliases": {
"Quantity": 1,
"Items": ["yourdomain.com"]
},
"ViewerCertificate": {
"ACMCertificateArn": "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxx",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
},
"HttpVersion": "http2and3",
"PriceClass": "PriceClass_200"
}'
Giải thích các config quan trọng:
OriginProtocolPolicy: https-only: CloudFront giao tiếp với ALB qua HTTPS — encrypted end-to-endViewerProtocolPolicy: redirect-to-https: Tự redirect HTTP → HTTPS cho usersCachePolicyId:658327ea...là Managed-CachingOptimized — cache policy mặc định tốt cho hầu hết trường hợpOriginRequestPolicyId:216adef6...là Managed-AllViewer — forward tất cả headers, query strings, cookies tới originHttpVersion: http2and3: Bật HTTP/3 (QUIC) — nhanh hơn cho mobile usersPriceClass_200: Dùng edge locations ở tất cả regions trừ South America — balance giữa giá và performance
Cache behaviors cho API và static assets
API requests (/api/*) không nên cache — mỗi request phải tới backend. Static assets thì cache aggressively:
# Behavior cho /api/* — KHÔNG cache, forward tất cả
aws cloudfront create-distribution \
# ... thêm CacheBehaviors:
"CacheBehaviors": {
"Quantity": 2,
"Items": [
{
"PathPattern": "/api/*",
"TargetOriginId": "alb-origin",
"ViewerProtocolPolicy": "https-only",
"AllowedMethods": {
"Quantity": 7,
"Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"]
},
"CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
"OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3"
},
{
"PathPattern": "/_nuxt/*",
"TargetOriginId": "alb-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true
}
]
}
/api/*:CachePolicyId=4135ea2d...là Managed-CachingDisabled — pass qua CloudFront thẳng tới ALB/_nuxt/*: Nuxt 3 build static assets vào/_nuxt/với hashed filenames — cache vĩnh viễn cũng không sao vì mỗi build tạo filename mới
Bước 5: Route 53 — Trỏ domain về CloudFront
Giờ trỏ domain về CloudFront distribution:
aws route53 change-resource-record-sets \
--hosted-zone-id ZONE_ID \
--change-batch '{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "yourdomain.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d1234abcdef.cloudfront.net",
"EvaluateTargetHealth": false
}
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "yourdomain.com",
"Type": "AAAA",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d1234abcdef.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}
]
}'
Một vài điểm quan trọng:
Z2FDTNDATAQYW2: Đây là Hosted Zone ID cố định của CloudFront — không phải zone ID của bạn, mà là constant dùng chung cho tất cả CloudFront distributions- Record A + AAAA: A cho IPv4, AAAA cho IPv6 — CloudFront hỗ trợ cả hai
- Alias record: Không phải CNAME — alias hoạt động ở root domain (
yourdomain.com), CNAME thì không
Nếu muốn www.yourdomain.com cũng hoạt động:
# www → redirect về root domain
aws route53 change-resource-record-sets \
--hosted-zone-id ZONE_ID \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.yourdomain.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d1234abcdef.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}]
}'
Nhớ thêm www.yourdomain.com vào Aliases của CloudFront distribution.
Bước 6: WAF — Bảo vệ khỏi tấn công
AWS WAF đặt trước CloudFront, filter traffic trước khi tới ứng dụng. Setup cơ bản nhưng hiệu quả:
# Tạo Web ACL cho CloudFront (phải ở us-east-1)
aws wafv2 create-web-acl \
--name myapp-waf \
--scope CLOUDFRONT \
--region us-east-1 \
--default-action '{"Allow": {}}' \
--rules '[
{
"Name": "AWS-AWSManagedRulesCommonRuleSet",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "CommonRuleSet"
}
},
{
"Name": "AWS-AWSManagedRulesKnownBadInputsRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesKnownBadInputsRuleSet"
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "KnownBadInputs"
}
},
{
"Name": "AWS-AWSManagedRulesSQLiRuleSet",
"Priority": 3,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesSQLiRuleSet"
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "SQLiRuleSet"
}
},
{
"Name": "RateLimit",
"Priority": 4,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit"
}
}
]' \
--visibility-config '{
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "myapp-waf"
}'
4 rules này cover phần lớn threats:
- AWSManagedRulesCommonRuleSet: Chặn các attack pattern phổ biến (XSS, path traversal, bad bots…)
- AWSManagedRulesKnownBadInputsRuleSet: Chặn request patterns đã biết là malicious (Log4j, Java deserialization…)
- AWSManagedRulesSQLiRuleSet: Chặn SQL injection
- RateLimit 2000/5min per IP: Chặn brute force và DDoS cơ bản — 1 IP gửi quá 2000 requests trong 5 phút sẽ bị block
Attach WAF vào CloudFront:
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:ACCOUNT_ID:global/webacl/myapp-waf/xxx \
--resource-arn arn:aws:cloudfront::ACCOUNT_ID:distribution/DIST_ID
Thêm rule cho API rate limiting chặt hơn
API endpoints thường cần rate limit thấp hơn trang chủ:
# Rate limit riêng cho /api/* — 500 requests/5 phút per IP
{
"Name": "APIRateLimit",
"Priority": 0,
"Statement": {
"RateBasedStatement": {
"Limit": 500,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/api/",
"FieldToMatch": {"UriPath": {}},
"PositionalConstraint": "STARTS_WITH",
"TextTransformations": [{"Priority": 0, "Type": "NONE"}]
}
}
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "APIRateLimit"
}
}
Bước 7: Auto Scaling
Hệ thống cần tự scale khi traffic tăng:
# Đăng ký service với Application Auto Scaling
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/myapp-cluster/myapp-service \
--min-capacity 2 \
--max-capacity 10
# Scale dựa trên CPU utilization
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/myapp-cluster/myapp-service \
--policy-name cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
# Scale dựa trên request count (qua ALB)
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/myapp-cluster/myapp-service \
--policy-name request-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 1000.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/myapp-alb/xxx/targetgroup/frontend-tg/yyy"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
Hai policies hoạt động song song:
- CPU trên 70%? Scale out
- Mỗi target nhận quá 1000 requests/phút? Scale out
- Traffic giảm? Scale in sau 5 phút cooldown
ScaleOutCooldown: 60 nghĩa là scale out nhanh (1 phút), nhưng ScaleInCooldown: 300 scale in chậm (5 phút) — tránh hiện tượng “flapping” khi traffic dao động.
Bước 8: Security Groups — Khóa chặt traffic
Một sai lầm phổ biến: để ALB nhận traffic từ mọi nơi. Khi đã có CloudFront, ALB chỉ nên nhận traffic từ CloudFront:
# Security Group cho ALB — chỉ cho phép CloudFront
aws ec2 create-security-group \
--group-name myapp-alb-sg \
--description "ALB - CloudFront only"
# AWS publish danh sách IP ranges của CloudFront
# Dùng AWS-managed prefix list thay vì hardcode IPs
aws ec2 authorize-security-group-ingress \
--group-id sg-alb-xxx \
--ip-permissions '[
{
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"PrefixListIds": [{"PrefixListId": "pl-3b927c52"}]
}
]'
pl-3b927c52 là AWS-managed prefix list cho CloudFront IPs (ở ap-southeast-1). AWS tự cập nhật list này khi CloudFront thêm IP mới — bạn không cần maintain.
# Security Group cho ECS tasks — chỉ nhận từ ALB
aws ec2 authorize-security-group-ingress \
--group-id sg-ecs-xxx \
--ip-permissions '[
{
"IpProtocol": "tcp",
"FromPort": 3000,
"ToPort": 3000,
"UserIdGroupPairs": [{"GroupId": "sg-alb-xxx"}]
},
{
"IpProtocol": "tcp",
"FromPort": 8000,
"ToPort": 8000,
"UserIdGroupPairs": [{"GroupId": "sg-alb-xxx"}]
}
]'
Traffic flow bây giờ: Internet → CloudFront → WAF → ALB (chỉ từ CloudFront) → ECS (chỉ từ ALB). Không ai bypass được CloudFront/WAF để tấn công trực tiếp vào ALB hay containers.
Bước 9: Monitoring và Alerting
Hệ thống chạy rồi, nhưng bạn cần biết khi nào nó có vấn đề — trước khi users báo lỗi.
CloudWatch Alarms
# Alarm khi ECS service unhealthy
aws cloudwatch put-metric-alarm \
--alarm-name myapp-unhealthy-tasks \
--namespace AWS/ECS \
--metric-name CPUUtilization \
--dimensions Name=ClusterName,Value=myapp-cluster Name=ServiceName,Value=myapp-service \
--statistic Average \
--period 60 \
--threshold 90 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 3 \
--alarm-actions arn:aws:sns:ap-southeast-1:ACCOUNT_ID:myapp-alerts
# Alarm khi ALB 5xx errors tăng
aws cloudwatch put-metric-alarm \
--alarm-name myapp-5xx-errors \
--namespace AWS/ApplicationELB \
--metric-name HTTPCode_Target_5XX_Count \
--dimensions Name=LoadBalancer,Value=app/myapp-alb/xxx \
--statistic Sum \
--period 300 \
--threshold 50 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:ap-southeast-1:ACCOUNT_ID:myapp-alerts
# Alarm khi WAF block rate cao bất thường
aws cloudwatch put-metric-alarm \
--alarm-name myapp-waf-blocks \
--namespace AWS/WAFV2 \
--metric-name BlockedRequests \
--dimensions Name=WebACL,Value=myapp-waf Name=Rule,Value=ALL \
--statistic Sum \
--period 300 \
--threshold 1000 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:ap-southeast-1:ACCOUNT_ID:myapp-alerts \
--region us-east-1
Setup SNS topic để nhận alert qua email hoặc Slack:
aws sns create-topic --name myapp-alerts
aws sns subscribe \
--topic-arn arn:aws:sns:ap-southeast-1:ACCOUNT_ID:myapp-alerts \
--protocol email \
--notification-endpoint your-email@example.com
Chi phí tổng thể
Với setup production-ready hoàn chỉnh (2 ECS tasks, CloudFront, WAF, Route 53):
| Service | Chi phí/tháng |
|---|---|
| ECS Fargate (0.5 vCPU, 1GB, 2 tasks) | ~$30 |
| ALB | ~$20 |
| CloudFront (100GB transfer) | ~$10 |
| Route 53 (1 hosted zone + queries) | ~$1 |
| ACM (SSL certificates) | $0 |
| WAF (3 managed rules + rate limit) | ~$12 |
| CloudWatch (logs + alarms) | ~$5 |
| ECR (10 images) | ~$1 |
| Tổng | ~$79/tháng |
So với tự setup Nginx + Let’s Encrypt + fail2ban trên VPS ($15/tháng), giá cao hơn nhưng bạn được:
- Auto-scaling khi traffic tăng đột biến
- CDN toàn cầu — users ở US, EU truy cập nhanh như local
- WAF managed rules — AWS update rules khi có threat mới
- Zero-downtime deployment mỗi lần push code
- Không cần thức đêm vì server chết
Checklist hoàn chỉnh
Tổng hợp toàn bộ 2 phần, đây là checklist để đưa một hệ thống Nuxt.js + FastAPI lên production trên AWS:
Phần 1 — Core Infrastructure:
- Dockerize frontend (Nuxt.js) và backend (FastAPI) với multi-stage builds
- Tạo ECR repositories, push images
- Setup ECS Cluster + Task Definition + Service trên Fargate
- Cấu hình ALB với target groups và routing rules
- Viết GitHub Actions CI/CD workflow
Phần 2 — Production-Ready: 6. Request SSL certificates qua ACM (us-east-1 cho CloudFront + region local cho ALB) 7. Tạo Route 53 hosted zone, cập nhật name servers ở registrar 8. Bật HTTPS trên ALB (listener 443 + redirect 80→443) 9. Setup CloudFront distribution với cache behaviors riêng cho API và static assets 10. Trỏ domain về CloudFront qua Route 53 alias records 11. Tạo WAF Web ACL với managed rules + rate limiting 12. Lock security groups: ALB chỉ nhận từ CloudFront, ECS chỉ nhận từ ALB 13. Setup auto-scaling policies (CPU + request count) 14. Cấu hình CloudWatch alarms + SNS notifications
14 bước nghe nhiều, nhưng phần lớn chỉ cần setup một lần. Sau đó, workflow hàng ngày chỉ là push code — mọi thứ tự động.
Bạn đã từng setup hệ thống trên AWS chưa? Có phần nào thấy khó hiểu hoặc muốn mình đi sâu hơn? Chia sẻ qua email nhé!