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é!
cat comments.log