ELB & Auto Scaling: shop-app không còn phụ thuộc một EC2

5 câu hỏi~45 phútTừ khóa: ALB, NLB, GWLB, target group, health check, Auto Scaling Group, launch template, target tracking, cooldown, warm-up, sticky session, connection draining · SAA: Domain 2 Resilient

Tiếp đi: Load Balancer và Auto Scaling. Bài trước mình đã có một EC2 t3.micro chạy shop-app.jar trong shop-public-a. Hôm qua mình chạy sudo reboot để thử, website chết đúng 2 phút và không ai biết. Giờ muốn chạy ít nhất 2 máy và có gì đó tự thay máy hỏng.

Đây chính là bước chuyển từ "một server" sang "một nhóm server" và là trọng tâm của Domain 2 – Design Resilient Architectures trong đề SAA. Hai mảnh ghép: Elastic Load Balancing (ELB) chia request cho nhiều instance, và Auto Scaling Group (ASG) giữ số instance đúng mong muốn, tự thay máy hỏng, tự tăng giảm theo tải.

1. Bài toán: shop-app chạy trên một EC2

Sau bài 22.4 EC2, kiến trúc đang là:

Internet
   |
Elastic IP 13.x.x.x  ──→  EC2 shop-app (t3.micro, shop-public-a)
                              └─ shop-app.jar :8080
                              └─ /actuator/health

Người dùng gõ thẳng IP (hoặc DNS trỏ vào IP). Mọi thứ phụ thuộc đúng một máy trong đúng một Availability Zone.

2. Vấn đề

3. Giải pháp: ALB phía trước + ASG phía sau

                    Internet
                       |
              ┌────────▼────────┐
              │   shop-alb      │  Application Load Balancer (public, 2 AZ)
              │ listener :80/443│  SG: shop-alb-sg (80/443 từ 0.0.0.0/0)
              └────────┬────────┘
                       │ forward
              ┌────────▼────────┐
              │   shop-tg       │  Target group: HTTP:8080, health /actuator/health
              └───┬─────────┬───┘
                  │         │
        ┌─────────▼──┐  ┌───▼────────┐
        │ EC2 (1a)   │  │ EC2 (1b)   │   shop-asg: min 2, desired 2, max 4
        │ private-a  │  │ private-b  │   launch template shop-lt
        │ shop-app-sg│  │ shop-app-sg│   (8080 chỉ từ shop-alb-sg)
        └────────────┘  └────────────┘

Các thành phần và thuật ngữ đề thi cần nắm chắc:

Thành phầnVai tròTừ khóa đề thi
Load balancerĐiểm vào duy nhất có DNS name (shop-alb-123.ap-southeast-1.elb.amazonaws.com), trải trên nhiều AZinternet-facing vs internal, cross-zone load balancing
ListenerCổng + giao thức LB lắng nghe (HTTP:80, HTTPS:443). Mỗi listener có ruleslistener rule, host/path-based routing, redirect 80→443
Target groupNhóm đích (instance / IP / Lambda / ALB) + health checktarget type, deregistration delay (connection draining), slow start
Health checkLB gọi /actuator/health định kỳ; fail N lần thì ngừng gửi traffichealthy/unhealthy threshold, interval, matcher 200
Launch template"Bản thiết kế" instance: AMI, type, SG, user data, IAM rolelaunch template (mới) vs launch configuration (deprecated)
Auto Scaling GroupGiữ desired instance trong khoảng [min, max], trải nhiều subnet/AZmin/max/desired, AZ rebalancing, lifecycle hook, instance refresh
Scaling policyLuật tăng/giảm desiredtarget tracking, step, simple, scheduled, predictive

Ba loại ELB hiện còn trong đề (Classic Load Balancer đã là legacy, không nên chọn):

ALBNLBGWLB
Layer7 (HTTP/HTTPS/gRPC)4 (TCP/UDP/TLS)3 (IP packet, GENEVE 6081)
RoutingPath, host, header, query string, methodTheo port/protocol; giữ nguyên IP clientChuyển mọi gói sang appliance (firewall, IDS)
Static IPKhông (DNS name, IP đổi) → đặt sau Global Accelerator nếu cầnCó, 1 static IP / Elastic IP mỗi AZKhông
Hiệu năngMs-level, đủ cho web/APIHàng triệu req/s, latency rất thấpTransparent
TargetInstance, IP, Lambda, ALB khôngInstance, IP, ALBInstance, IP
TLSTerminate, ACM cert, SNI nhiều certTLS listener terminate hoặc pass-throughKhông
Dùng cho shop-appREST API, web, redirect, WAFGame/IoT TCP, cần IP cố định whitelist, PrivateLinkTích hợp firewall bên thứ ba

Những tính năng ALB mà đề thi và dự án thật hay cần (đều cấu hình trên listener hoặc target group, không đụng vào code):

Tính năngÝ nghĩa với shop-appGhi chú
Listener rule theo path/host/header/queryMột ALB phục vụ /api/* → shop-tg, /admin/* → admin-tg, static.shop.example.com → fixed response hoặc redirect sang CloudFrontTối đa 100 rule/listener mặc định; rule có priority, default rule cuối cùng
Weighted target groupsRule forward 90% vào shop-tg-blue, 10% vào shop-tg-green → canary / blue-greenCó thể bật stickiness theo target group để user không nhảy giữa 2 version
Redirect / fixed-responseListener 80 redirect 301 sang 443; trả 503 "maintenance" không cần backendKhông tốn target
Authenticate-OIDC / CognitoALB tự làm login với Cognito hoặc Keycloak (OIDC) trước khi forward, gửi claim qua header x-amzn-oidc-dataHữu ích cho trang admin nội bộ; app public vẫn nên tự validate JWT (Resource Server)
Cross-zone load balancingNode ALB ở AZ 1a có thể gửi tới target ở 1b → tải đều dù số instance mỗi AZ lệchALB: luôn bật, miễn phí. NLB: tắt mặc định, bật thì tính phí data liên AZ
Slow startTarget mới nhận tải tăng dần trong 30–900s, cho JVM warm-up (JIT, pool)Rất hợp với Spring Boot; đặt 60–120s
Access log → S3Mỗi request: client IP, latency từng chặng, target, statusDùng Athena query khi điều tra 5xx (bài 22.17)
Idle timeoutMặc định 60s; app có long-polling/SSE cần tăngPhải nhỏ hơn keep-alive timeout của Tomcat để tránh 502 lẻ tẻ
HTTP/2, gRPC, mTLSALB nói HTTP/2 với client, HTTP/1.1 với target (hoặc gRPC end-to-end)mTLS listener xác thực client cert ở ALB
Lambda targetMột path trong cùng ALB trỏ sang LambdaCách trộn EC2 và serverless không cần API Gateway

Ví dụ hai rule hay dùng ngay cho shop-app:

# Redirect 80 → 443 (sau khi có cert ở bài 22.10)
aws elbv2 modify-listener --listener-arn $LISTENER_80_ARN \
  --default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}' --profile shop-lab

# Canary: 90/10 giữa hai target group trên listener 443
aws elbv2 create-rule --listener-arn $LISTENER_443_ARN --priority 10 \
  --conditions Field=path-pattern,Values='/api/*' \
  --actions 'Type=forward,ForwardConfig={TargetGroups=[{TargetGroupArn='$TG_BLUE',Weight=90},{TargetGroupArn='$TG_GREEN',Weight=10}],TargetGroupStickinessConfig={Enabled=true,DurationSeconds=600}}' \
  --profile shop-lab

4. Cấu hình từng bước (CLI + Java)

Bước 1: Security group. Nguyên tắc chain: Internet → shop-alb-sg → shop-app-sg. Instance không nhận request trực tiếp từ Internet nữa.

export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --profile shop-lab)
VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=shop-vpc \
  --query 'Vpcs[0].VpcId' --output text --profile shop-lab)

ALB_SG=$(aws ec2 create-security-group --group-name shop-alb-sg \
  --description "ALB 80/443 from internet" --vpc-id $VPC_ID \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=shop-alb-sg},{Key=Project,Value=shop-app},{Key=Env,Value=lab}]' \
  --query GroupId --output text --profile shop-lab)
aws ec2 authorize-security-group-ingress --group-id $ALB_SG \
  --protocol tcp --port 80 --cidr 0.0.0.0/0 --profile shop-lab

# shop-app-sg đã có từ bài 22.4: chỉ cho 8080 từ ALB SG, bỏ rule 8080 từ 0.0.0.0/0 nếu còn
APP_SG=$(aws ec2 describe-security-groups --filters Name=group-name,Values=shop-app-sg \
  --query 'SecurityGroups[0].GroupId' --output text --profile shop-lab)
aws ec2 authorize-security-group-ingress --group-id $APP_SG \
  --protocol tcp --port 8080 --source-group $ALB_SG --profile shop-lab

Bước 2: Target group. Health check trỏ vào Actuator (xem Actuator).

TG_ARN=$(aws elbv2 create-target-group --name shop-tg \
  --protocol HTTP --port 8080 --vpc-id $VPC_ID --target-type instance \
  --health-check-protocol HTTP --health-check-path /actuator/health \
  --health-check-interval-seconds 15 --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 --unhealthy-threshold-count 3 \
  --matcher HttpCode=200 \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab \
  --query 'TargetGroups[0].TargetGroupArn' --output text --profile shop-lab)

# Deregistration delay: thời gian chờ request đang chạy xong trước khi gỡ target (mặc định 300s)
aws elbv2 modify-target-group-attributes --target-group-arn $TG_ARN \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30 --profile shop-lab

Bước 3: ALB + listener. ALB bắt buộc ít nhất 2 subnet ở 2 AZ khác nhau, và phải là public subnet nếu internet-facing.

PUB_A=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-public-a --query 'Subnets[0].SubnetId' --output text --profile shop-lab)
PUB_B=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-public-b --query 'Subnets[0].SubnetId' --output text --profile shop-lab)

ALB_ARN=$(aws elbv2 create-load-balancer --name shop-alb \
  --type application --scheme internet-facing --ip-address-type ipv4 \
  --subnets $PUB_A $PUB_B --security-groups $ALB_SG \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text --profile shop-lab)

aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
  --protocol HTTP --port 80 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN --profile shop-lab

Khi có domain và ACM cert (bài 22.10), thêm listener 443 và đổi listener 80 thành Type=redirect sang HTTPS.

Bước 4: Launch template. Tận dụng user data từ bài 22.4; app phải lấy jar và chạy được không cần người gõ lệnh, vì ASG sẽ tạo instance lúc không ai ngồi đó.

cat > user-data.sh <<'EOF'
#!/bin/bash
dnf install -y java-21-amazon-corretto-headless
aws s3 cp s3://shop-app-assets-ACCOUNT_ID/releases/shop-app.jar /opt/shop-app.jar
cat > /etc/systemd/system/shop-app.service <<'UNIT'
[Unit]
Description=shop-app
After=network.target
[Service]
Environment=SPRING_PROFILES_ACTIVE=aws
ExecStart=/usr/bin/java -jar /opt/shop-app.jar
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
systemctl enable --now shop-app
EOF
sed -i "s/ACCOUNT_ID/$ACCOUNT_ID/" user-data.sh

AMI_ID=$(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query Parameter.Value --output text --profile shop-lab)

aws ec2 create-launch-template --launch-template-name shop-lt \
  --tag-specifications 'ResourceType=launch-template,Tags=[{Key=Project,Value=shop-app},{Key=Env,Value=lab}]' \
  --launch-template-data "{
    \"ImageId\": \"$AMI_ID\",
    \"InstanceType\": \"t3.micro\",
    \"IamInstanceProfile\": {\"Name\": \"shop-app-ec2-role\"},
    \"SecurityGroupIds\": [\"$APP_SG\"],
    \"UserData\": \"$(base64 -w0 user-data.sh)\",
    \"TagSpecifications\": [{\"ResourceType\": \"instance\",
      \"Tags\": [{\"Key\":\"Name\",\"Value\":\"shop-app\"},{\"Key\":\"Project\",\"Value\":\"shop-app\"},{\"Key\":\"Env\",\"Value\":\"lab\"}]}]
  }" --profile shop-lab

Không có AssociatePublicIpAddress: instance nằm trong private subnet, ra Internet qua NAT (bài 22.3). Role shop-app-ec2-role cần quyền đọc bucket để tải jar.

Bước 5: Auto Scaling Group. Chú ý --health-check-type ELB: ASG dùng kết quả health check của target group để quyết định thay instance, thay vì chỉ nhìn EC2 status check.

PRIV_A=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-private-a --query 'Subnets[0].SubnetId' --output text --profile shop-lab)
PRIV_B=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-private-b --query 'Subnets[0].SubnetId' --output text --profile shop-lab)

aws autoscaling create-auto-scaling-group --auto-scaling-group-name shop-asg \
  --launch-template LaunchTemplateName=shop-lt,Version='$Latest' \
  --min-size 2 --max-size 4 --desired-capacity 2 \
  --vpc-zone-identifier "$PRIV_A,$PRIV_B" \
  --target-group-arns $TG_ARN \
  --health-check-type ELB --health-check-grace-period 120 \
  --tags Key=Project,Value=shop-app,PropagateAtLaunch=true Key=Env,Value=lab,PropagateAtLaunch=true \
  --profile shop-lab

Bước 6: Scaling policy (target tracking). "Giữ CPU trung bình quanh 50%". ASG tự tạo 2 CloudWatch alarm (cao/thấp).

aws autoscaling put-scaling-policy --auto-scaling-group-name shop-asg \
  --policy-name shop-cpu-50 --policy-type TargetTrackingScaling \
  --target-tracking-configuration '{
    "PredefinedMetricSpecification": {"PredefinedMetricType": "ASGAverageCPUUtilization"},
    "TargetValue": 50.0
  }' \
  --estimated-instance-warmup 120 --profile shop-lab

Với app web, metric ALBRequestCountPerTarget (số request/instance) thường phản ánh tải đúng hơn CPU, đặc biệt khi app chờ DB/IO nhiều.

Bước 7: Spring Boot phía sau ALB. Ba thứ hay quên:

# application-aws.yml
server:
  port: 8080
  forward-headers-strategy: framework   # tin X-Forwarded-For / X-Forwarded-Proto từ ALB
  shutdown: graceful                     # chờ request đang xử lý khi SIGTERM
spring:
  lifecycle:
    timeout-per-shutdown-phase: 25s      # < deregistration delay 30s
management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        readiness:
          include: readinessState, db     # ALB nên dùng /actuator/health/readiness
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

5. Cách hoạt động

Một request đi qua ALB:

Client ──DNS──→ shop-alb-xxx.elb.amazonaws.com (trả 2 IP, mỗi AZ 1 node ALB)
   │
   ▼
ALB node (AZ 1a)  ── listener :80 rule: default forward shop-tg
   │   cross-zone LB (ALB mặc định bật): chọn target ở 1a hoặc 1b
   │   thêm header X-Forwarded-For: <client ip>, X-Forwarded-Proto: http
   ▼
EC2 10.0.12.23:8080 (private-b) ── Spring MVC xử lý ── 200 OK
   │
   ▼
ALB trả về client, ghi access log (nếu bật) vào S3

Vòng lặp của ASG (control loop, giống ReplicaSet trong Kubernetes):

mỗi ~phút:
  actual = số instance InService
  if actual < desired: launch (chọn AZ ít instance hơn → cân bằng AZ)
  if actual > desired: terminate theo termination policy
                        (mặc định: AZ nhiều nhất → launch template cũ → gần giờ tính tiền)
  với mỗi instance: nếu ELB báo unhealthy quá grace period → terminate + launch mới

scaling policy:
  CloudWatch alarm CPU > 50% (3 datapoint)  → desired += N  (scale out)
  CloudWatch alarm CPU < 50% - margin, lâu   → desired -= 1  (scale in, thận trọng)
  warm-up 120s: instance mới chưa tính vào metric → tránh scale out dồn dập

Thứ tự thời gian khi một instance chết:

t=0     app crash (OOM)                  → /actuator/health timeout
t=15s   health check fail #1
t=30s   fail #2
t=45s   fail #3 → target Unhealthy → ALB ngừng gửi request (user không còn thấy lỗi)
t=~60s  ASG thấy ELB unhealthy → đánh dấu Unhealthy → terminate
t=~90s  launch instance mới, user data chạy (cài Java, tải jar) ≈ 60–90s
t=~3m   instance mới Healthy → desired 2 lại đủ 2

Trong 3 phút đó, instance còn lại gánh 100% tải. Đó là lý do min 2 và trải 2 AZ là tối thiểu cho production; đề thi dùng cụm từ "highly available" là ám chỉ điểm này.

Metric CloudWatch cần nhìn (namespace AWS/ApplicationELB, dimension LoadBalancer và TargetGroup). Đây cũng là nền cho alarm shop-alb-5xx-high ở bài 22.15:

MetricÝ nghĩaKhi nào lo
HealthyHostCount / UnHealthyHostCountSố target healthy theo từng AZHealthy < 2 → mất HA; alarm ngay
HTTPCode_Target_5XX_Count5xx do app trảBug, DB lỗi; đọc log app
HTTPCode_ELB_5XX_Count5xx do ALB tự sinh (502/503/504)502: app đóng kết nối sớm / keep-alive lệch; 503: không còn target healthy; 504: target quá idle timeout
TargetResponseTime (p99)Thời gian từ ALB gửi tới nhận byte đầuTăng dần theo RequestCount → thiếu instance hoặc DB pool đầy
RequestCountPerTargetRequest trung bình mỗi instanceMetric tốt cho target tracking của app web
ActiveConnectionCount, RejectedConnectionCountKết nối đang mở / bị từ chối vì target hết chỗRejected > 0 → tăng max của ASG hoặc Tomcat max-connections
GroupInServiceInstances (AWS/AutoScaling)Số instance ASG đang phục vụPhải bật metric collection của ASG (enable-metrics-collection)

Một quy tắc đọc nhanh: Target_5XX là lỗi của dev Java, ELB_5XX là lỗi của hạ tầng/cấu hình (SG, timeout, không còn target). Phỏng vấn hỏi "502 lẻ tẻ sau ALB thì debug sao" là đang kiểm tra bạn có phân biệt được hai metric này.

6. So sánh

Các loại scaling policy — đề SAA rất hay hỏi "chọn policy nào":

PolicyCách hoạt độngChọn khi đề nói
Target trackingĐặt mục tiêu (CPU 50%, 1000 req/target), AWS tự tính thêm/bớt bao nhiêu"simplest", "least operational overhead", tải biến đổi tự nhiên
Step scalingNhiều bậc: CPU 60–70% +1, 70–85% +2, >85% +4Cần kiểm soát tinh theo mức alarm
Simple scalingMột alarm → một hành động, có cooldown chờ xong mới xét tiếpHiếm khi là đáp án đúng (cũ)
ScheduledĐặt desired theo lịch (8h sáng lên 4, 22h xuống 2)"predictable traffic", "every Monday 9 AM", "known schedule"
PredictiveML học lịch sử 14 ngày, scale trước khi tải đến"recurring pattern", "proactively", cần cả scheduled-like và adaptive

Health check: EC2 vs ELB

EC2 status checkELB health check
Kiểm traHypervisor/network của instanceHTTP tới /actuator/health của app
Phát hiệnMáy chết, mất mạngApp treo, OOM, DB mất kết nối mà máy vẫn sống
ASG dùngMặc địnhPhải bật --health-check-type ELB

Scale out vs scale up, ASG vs K8s HPA

Vertical (scale up)Horizontal (ASG)K8s HPA + Cluster Autoscaler
Đơn vịĐổi instance typeThêm/bớt EC2Thêm/bớt Pod; node do CA/Karpenter
DowntimePhải stop/startKhôngKhông
Thời gian phản ứngPhút + thủ công2–4 phút (boot + user data)Giây (image đã pull) + phút nếu cần node
Yêu cầu appKhôngStateless, session ngoài (Redis/DynamoDB)Stateless, container

7. Khi nào dùng / không

Mình chạy 2 instance xong thì user cứ bị đăng xuất ngẫu nhiên. Mình thấy ALB có nút stickiness, bật lên là xong đúng không?

Bật stickiness thì triệu chứng biến mất, nhưng bạn đang che bệnh chứ chưa chữa. Hãy phân biệt hai cách:

Sticky session (ALB stickiness): ALB gắn cookie AWSALB (duration-based) hoặc dùng cookie của app (app-based, ví dụ JSESSIONID) để ghim user vào một target.

aws elbv2 modify-target-group-attributes --target-group-arn $TG_ARN --attributes \
  Key=stickiness.enabled,Value=true \
  Key=stickiness.type,Value=lb_cookie \
  Key=stickiness.lb_cookie.duration_seconds,Value=3600 --profile shop-lab

Hệ quả:

user A,B,C,D,E ghim vào EC2-1 (ngẫu nhiên lúc đầu)
user F         ghim vào EC2-2
→ tải lệch: EC2-1 90% CPU, EC2-2 nhàn rỗi → ASG scale out thêm EC2-3 nhưng user cũ vẫn dính EC2-1
→ EC2-1 bị ASG thay (unhealthy) → A..E mất session cùng lúc

Cách đúng: externalize session để instance nào cũng trả lời được. Trên AWS có hai lựa chọn hay gặp:

ElastiCache Redis + Spring SessionDynamoDB shop-sessionsJWT stateless
Thay đổi codeThêm spring-session-data-redis, gần như không sửa codeSpring Session chưa có store chính thức; tự viết hoặc dùng lib cộng đồngĐổi mô hình auth
LatencySub-ms trong VPCVài ms0 (không gọi store)
Chi phí vận hànhCluster Redis (không Free Tier)Serverless, pay per request, TTL tự xóaKhông có store; revoke khó
Bài liên quan22.9, Redis cache22.8JWT

Với shop-app, bài 22.9 sẽ bật Spring Session Redis. Còn bây giờ, bật stickiness tạm để lab chạy được là chấp nhận được, miễn bạn hiểu nó không phải đích đến. Trong đề SAA, khi câu hỏi nói "users are randomly logged out after adding instances", đáp án đúng gần như luôn là ElastiCache/DynamoDB để lưu session, không phải sticky session; sticky session chỉ đúng khi đề nói rõ "legacy application cannot be modified".

Hai thắc mắc nữa. Một là hóa đơn tháng này có dòng "LoadBalancerUsage" dù mình gần như không có traffic. Hai là ASG của mình cứ tạo instance mới rồi khoảng 2 phút sau terminate, lặp đi lặp lại, log không thấy gì.

Cả hai đều rất hay gặp.

1. ALB tính tiền theo giờ, không phải theo traffic. Mỗi ALB tính phí giờ tồn tại (khoảng vài chục USD/tháng nếu để cả tháng, kiểm tra pricing page của region) cộng với LCU (Load Balancer Capacity Unit) theo số kết nối/băng thông/rule. Free Tier chỉ cho 750 giờ ALB trong 12 tháng đầu của tài khoản mới; hết thời hạn hoặc tạo 2 ALB là trả tiền ngay dù 0 request. Vì vậy trong lab luôn xóa ALB khi xong, và trong dự án thật, nhiều service nên dùng chung một ALB với listener rule theo host/path thay vì mỗi service một ALB.

shop-alb :443
  ├─ rule host api.shop.example.com, path /orders/*   → shop-tg
  ├─ rule host api.shop.example.com, path /payments/* → payment-tg
  └─ default                                          → web-tg

2. Vòng lặp "launch → terminate" = health check fail trong grace period quá ngắn. Diễn biến:

launch → instance Pending → InService (ASG tính giờ grace period 120s)
user data: dnf install java (40s) + tải jar (10s) + Spring Boot start (30–40s) + Hibernate/Flyway (20s)
→ tổng ~110–130s, có khi quá 120s
→ ALB health check vẫn fail khi hết grace → ASG: Unhealthy → terminate → launch lại → ...

Cách tìm nguyên nhân (đây là thứ bạn nên nhớ hơn cả cách sửa):

# Lịch sử ASG: lý do terminate
aws autoscaling describe-scaling-activities --auto-scaling-group-name shop-asg \
  --max-items 5 --query 'Activities[].[StartTime,StatusCode,Cause]' --output table --profile shop-lab
# → "...ELB health check failed..." hoặc "...taking EC2 instance out of service in response to ELB system health check failure"

# Trạng thái từng target và lý do
aws elbv2 describe-target-health --target-group-arn $TG_ARN --profile shop-lab
# "Reason": "Target.ResponseCodeMismatch" → app trả 503 (DB chưa kết nối) hay 401 (Actuator bị Security chặn!)
# "Reason": "Target.Timeout"              → SG không cho ALB vào 8080, hoặc app chưa lên

Sửa theo nguyên nhân:

Cho mình bài lab để tự tay làm phần này nhé.

Lab 22.5: shop-alb + shop-asg min 2 max 4 trải 2 AZ, tự thay instance hỏng

Mục tiêu

Chi phí & thời gian

Chuẩn bị

Các bước

Bước 1 – SG, target group, ALB, listener: chạy đúng các lệnh ở mục 4 (Bước 1–3). Lưu lại biến ALB_ARN, TG_ARN, APP_SG, ALB_SG.
Console: EC2 → Load Balancers → Create → Application Load Balancer → chọn VPC shop-vpc, 2 public subnet, SG shop-alb-sg → Listener HTTP:80 → Create target group (instance, HTTP 8080, health path /actuator/health).

ALB_DNS=$(aws elbv2 describe-load-balancers --names shop-alb \
  --query 'LoadBalancers[0].DNSName' --output text --profile shop-lab)
echo $ALB_DNS
# state phải là active (mất ~2 phút)
aws elbv2 describe-load-balancers --names shop-alb --query 'LoadBalancers[0].State.Code' --output text --profile shop-lab

Bước 2 – Launch template: chạy Bước 4 ở mục 4. Kiểm tra:

aws ec2 describe-launch-template-versions --launch-template-name shop-lt \
  --query 'LaunchTemplateVersions[0].LaunchTemplateData.[ImageId,InstanceType,IamInstanceProfile.Name]' \
  --output text --profile shop-lab

Console: EC2 → Launch Templates → Create: AMI Amazon Linux 2023, t3.micro, không chọn subnet (ASG quyết định), SG shop-app-sg, Advanced → IAM instance profile shop-app-ec2-role, User data dán script.

Bước 3 – ASG: chạy Bước 5 ở mục 4. Theo dõi:

watch -n 10 "aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names shop-asg \
  --query 'AutoScalingGroups[0].Instances[].[InstanceId,AvailabilityZone,LifecycleState,HealthStatus]' \
  --output table --profile shop-lab"

Chờ cả 2 dòng InService / Healthy, mỗi dòng một AZ (ap-southeast-1a, ap-southeast-1b).
Console: EC2 → Auto Scaling Groups → Create: launch template shop-lt, VPC shop-vpc, chọn shop-private-a + shop-private-b → Attach to existing load balancer → shop-tg → bật "Turn on Elastic Load Balancing health checks", grace 120 → min 2 / desired 2 / max 4 → tags.

Bước 4 – Scaling policy: chạy Bước 6 ở mục 4. Xem alarm ASG tự tạo:

aws cloudwatch describe-alarms --alarm-name-prefix TargetTracking-shop-asg \
  --query 'MetricAlarms[].[AlarmName,StateValue,Threshold]' --output table --profile shop-lab

Console: Auto Scaling Groups → shop-asg → Automatic scaling → Create dynamic scaling policy → Target tracking → Average CPU 50, warm-up 120.

Kiểm tra kết quả

# 1. Health qua ALB
curl -s http://$ALB_DNS/actuator/health
# {"status":"UP"}

# 2. Request chia cho 2 instance: thêm endpoint trả hostname, hoặc xem header/metadata
for i in $(seq 1 6); do curl -s http://$ALB_DNS/actuator/info | head -c 80; echo; done
# (nếu info có build info giống nhau, dùng access log hoặc CloudWatch RequestCountPerTarget)

aws elbv2 describe-target-health --target-group-arn $TG_ARN \
  --query 'TargetHealthDescriptions[].[Target.Id,Target.AvailabilityZone,TargetHealth.State]' \
  --output table --profile shop-lab
# 2 dòng, 2 AZ khác nhau, State = healthy

# 3. Metric RequestCount trên ALB (sau vài phút)
ALB_SUFFIX=${ALB_ARN#*loadbalancer/}
aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB --metric-name RequestCount \
  --dimensions Name=LoadBalancer,Value=$ALB_SUFFIX \
  --start-time $(date -u -d '-15 min' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
  --period 300 --statistics Sum --profile shop-lab

Thử thêm

A. Giết một instance, xem ASG thay.

VICTIM=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names shop-asg \
  --query 'AutoScalingGroups[0].Instances[0].InstanceId' --output text --profile shop-lab)
aws ec2 terminate-instances --instance-ids $VICTIM --profile shop-lab

# Trong lúc đó, gọi liên tục: không được có lỗi (hoặc chỉ 1–2 lỗi trong ~15s đầu trước khi health check phát hiện)
while true; do curl -s -o /dev/null -w "%{http_code} " http://$ALB_DNS/actuator/health; sleep 1; done

aws autoscaling describe-scaling-activities --auto-scaling-group-name shop-asg --max-items 3 \
  --query 'Activities[].[StartTime,Description,StatusCode]' --output table --profile shop-lab
# "Terminating EC2 instance: i-..." rồi "Launching a new EC2 instance: i-..."

Ghi lại: bao nhiêu giây từ terminate đến khi target mới healthy? Đó chính là "thời gian tự hồi phục" bạn sẽ nói trong phỏng vấn.

B. Tạo tải để scale out. Vào một instance qua Session Manager và đốt CPU:

aws ssm start-session --target $VICTIM_NEW --profile shop-lab
# trong máy:
sudo dnf install -y stress-ng && stress-ng --cpu 2 --timeout 600

Sau 3–5 phút, alarm TargetTracking-shop-asg-AlarmHigh chuyển ALARM, desired tăng lên 3 rồi 4. Dừng stress, chờ ~15 phút (scale in cố ý chậm hơn scale out) để desired về 2. Quan sát instance nào bị terminate: ASG ưu tiên AZ có nhiều instance hơn.

C. Instance refresh (deploy jar mới không downtime). Upload jar mới lên S3, tạo launch template version mới (hoặc giữ version nếu user data luôn tải bản mới nhất), rồi:

aws autoscaling start-instance-refresh --auto-scaling-group-name shop-asg \
  --preferences '{"MinHealthyPercentage": 50, "InstanceWarmup": 120}' --profile shop-lab
aws autoscaling describe-instance-refreshes --auto-scaling-group-name shop-asg --profile shop-lab

ASG thay từng 50% instance, chờ healthy mới thay tiếp. Đây là rolling update "kiểu AWS" cho EC2.

Dọn tài nguyên

Thứ tự quan trọng: ASG (kéo về 0 trước) → ALB listener/ALB → target group → launch template → SG. ALB và NAT Gateway là hai thứ tốn tiền từng giờ nếu quên.

# 1. ASG: về 0 rồi xóa (force-delete để không phải chờ)
aws autoscaling update-auto-scaling-group --auto-scaling-group-name shop-asg \
  --min-size 0 --desired-capacity 0 --profile shop-lab
aws autoscaling delete-auto-scaling-group --auto-scaling-group-name shop-asg --force-delete --profile shop-lab
# chờ instance terminated
aws ec2 describe-instances --filters Name=tag:Project,Values=shop-app Name=instance-state-name,Values=running,shutting-down \
  --query 'Reservations[].Instances[].InstanceId' --output text --profile shop-lab

# 2. ALB (listener xóa theo) và target group
aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN --profile shop-lab
sleep 30
aws elbv2 delete-target-group --target-group-arn $TG_ARN --profile shop-lab

# 3. Launch template
aws ec2 delete-launch-template --launch-template-name shop-lt --profile shop-lab

# 4. SG của ALB (shop-app-sg giữ lại cho bài sau; nếu xóa, phải gỡ rule tham chiếu trước)
aws ec2 revoke-security-group-ingress --group-id $APP_SG --protocol tcp --port 8080 --source-group $ALB_SG --profile shop-lab
aws ec2 delete-security-group --group-id $ALB_SG --profile shop-lab

# 5. Kiểm tra không còn gì tốn tiền
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName' --profile shop-lab
aws resourcegroupstaggingapi get-resources --tag-filters Key=Project,Values=shop-app \
  --query 'ResourceTagMappingList[].ResourceARN' --output text --profile shop-lab

Nếu bạn dừng lab ở đây và chưa làm bài 22.6/22.7 ngay, cân nhắc xóa cả NAT Gateway (bài 22.3) và tạo lại sau; nó không nằm trong Free Tier.

Phỏng vấn và đề thi SAA hỏi về ELB và Auto Scaling thì sao?

Q1. ALB và NLB khác nhau thế nào, bạn chọn gì cho REST API Spring Boot?

🇻🇳 Tiếng Việt:

ALB làm việc ở Layer 7: hiểu HTTP, route theo path/host/header, terminate TLS bằng ACM, gắn WAF, có thể trỏ tới Lambda. NLB ở Layer 4: chuyển TCP/UDP, giữ IP nguồn, có static IP mỗi AZ, latency rất thấp và chịu hàng triệu kết nối. REST API Spring Boot em chọn ALB vì cần path routing, redirect HTTPS và WAF. NLB em dùng khi cần IP cố định để đối tác whitelist, giao thức không phải HTTP, hoặc làm đầu vào PrivateLink.

🇬🇧 English:

ALB works at Layer 7: it understands HTTP, routes by path, host or header, terminates TLS with ACM certificates, integrates with WAF and can target Lambda. NLB works at Layer 4: it forwards TCP/UDP, preserves the client IP, provides a static IP per AZ and handles millions of connections with very low latency. For a Spring Boot REST API I pick ALB because I need path-based routing, HTTPS redirect and WAF. I use NLB when partners need a fixed IP to whitelist, for non-HTTP protocols, or as the entry point for PrivateLink.

Q2. ASG phát hiện và thay instance hỏng ra sao? Vì sao app của bạn phải stateless?

🇻🇳 Tiếng Việt:

ASG chạy một vòng lặp giữ số instance bằng desired. Em bật health check type ELB để ASG dùng kết quả HTTP health check của target group, vì EC2 status check chỉ biết máy sống, không biết app treo. Khi target unhealthy quá grace period, ASG terminate và launch instance mới từ launch template, user data tự cài và chạy app. Vì instance có thể bị thay bất kỳ lúc nào, app không được giữ state cục bộ: session để ở Redis hoặc DynamoDB, file upload lên S3, log đẩy ra CloudWatch.

🇬🇧 English:

The ASG runs a control loop that keeps the instance count equal to the desired capacity. I enable the ELB health check type so the ASG uses the target group's HTTP health check, because the EC2 status check only tells us the VM is alive, not that the app is healthy. When a target stays unhealthy past the grace period, the ASG terminates it and launches a replacement from the launch template, and user data installs and starts the app automatically. Since any instance can be replaced at any time, the app must be stateless: sessions in Redis or DynamoDB, uploads in S3, logs shipped to CloudWatch.

Q3. Deploy phiên bản mới cho ASG mà không downtime bằng cách nào?

🇻🇳 Tiếng Việt:

Có hai cách em hay dùng. Một là instance refresh: tạo launch template version mới (AMI hoặc jar mới), ASG thay dần instance với MinHealthyPercentage, chờ healthy rồi thay tiếp; kết hợp graceful shutdown của Spring Boot và deregistration delay của target group để không rớt request. Hai là blue/green: tạo ASG + target group mới, dùng listener rule weighted để chuyển dần traffic, có lỗi thì đổi weight về 0. Cách hai an toàn hơn nhưng tốn gấp đôi instance trong lúc deploy.

🇬🇧 English:

I use two approaches. First, instance refresh: publish a new launch template version with the new AMI or jar, and the ASG replaces instances gradually, respecting MinHealthyPercentage and waiting for health checks; combined with Spring Boot graceful shutdown and the target group's deregistration delay, no request is dropped. Second, blue/green: create a new ASG and target group, shift traffic with weighted listener rules, and roll back by setting the weight to zero. Blue/green is safer but doubles instance cost during the deployment.

Q4. Scale out thì nhanh, vì sao scale in phải cẩn thận hơn?

🇻🇳 Tiếng Việt:

Scale out thiếu máy chỉ tốn tiền, scale in sai thì rớt request và dao động liên tục. Em đặt warm-up để instance mới chưa tính vào metric, dùng target tracking vì nó scale in chậm hơn scale out có chủ ý, đặt deregistration delay và graceful shutdown để request đang chạy xong trước khi máy tắt, và bật scale-in protection cho instance đang chạy job dài hoặc dùng lifecycle hook để flush trước khi terminate.

🇬🇧 English:

Over-provisioning on scale out only costs money, but a wrong scale in drops requests and causes flapping. I set an instance warm-up so new instances do not skew the metric, use target tracking because it deliberately scales in more slowly than it scales out, configure deregistration delay plus graceful shutdown so in-flight requests finish, and use scale-in protection or lifecycle hooks for instances running long jobs that need to flush state before termination.

Góc thi SAA-C03

Câu 1: Web app Spring Boot trên EC2 sau ALB. Khi flash sale, người dùng báo bị đăng xuất ngẫu nhiên sau khi ASG thêm instance. Công ty muốn giải quyết với LEAST operational overhead và vẫn scale được. Chọn gì?
A. Bật sticky session trên target group
B. Lưu session vào Amazon ElastiCache for Redis bằng Spring Session
C. Tăng min size của ASG bằng max size để không còn scale
D. Chuyển sang NLB để giữ kết nối TCP

Đáp án: B. Externalize session để mọi instance đọc được. A che triệu chứng, gây lệch tải và mất session khi instance bị thay; chỉ đúng khi đề nói app legacy không sửa được. C bỏ luôn auto scaling, tốn tiền và không giải quyết instance bị thay khi hỏng. D NLB không liên quan tới session HTTP.

Câu 2: Một ứng dụng xử lý giao dịch cần IP tĩnh để ngân hàng đối tác whitelist, giao thức TCP tùy biến trên port 9000, hàng trăm nghìn kết nối với latency thấp nhất. Load balancer nào phù hợp?
A. Application Load Balancer
B. Classic Load Balancer
C. Network Load Balancer với Elastic IP mỗi AZ
D. Gateway Load Balancer

Đáp án: C. Từ khóa: static IP, TCP tùy biến, latency thấp → NLB. A ALB chỉ HTTP/HTTPS/gRPC và không có IP tĩnh. B CLB là legacy, hiếm khi là đáp án đúng. D GWLB dùng để chèn appliance bảo mật bên thứ ba, không phải LB cho app.

Câu 3: Hệ thống báo cáo nội bộ có tải tăng vọt đúng 8:00 sáng mỗi ngày làm việc và giảm sau 10:00. Target tracking hiện tại phản ứng chậm, người dùng chờ 5 phút đầu giờ. Giải pháp MOST cost-effective?
A. Tăng min size ASG lên bằng số instance giờ cao điểm
B. Thêm scheduled scaling action đặt desired capacity cao hơn lúc 7:45 và giảm lúc 10:15
C. Đổi sang simple scaling với cooldown ngắn
D. Đổi instance type lớn hơn

Đáp án: B. Tải "predictable, known schedule" → scheduled scaling, chuẩn bị máy trước giờ tải. A tốn tiền cả ngày. C vẫn reactive, không hết độ trễ boot. D vertical, tốn và vẫn có downtime khi đổi. Nếu đề nói "recurring pattern but not fixed time" thì đáp án là predictive scaling.

Cách nhớ ngắn gọn

Từ khóa đềNghĩ đến
path-based / host-based routing, WAF, redirect HTTPSALB
static IP, Elastic IP, TCP/UDP, millions of requests, PrivateLinkNLB
third-party firewall / IDS appliances, GENEVEGateway Load Balancer
highly available, replace unhealthy instances, multiple AZsASG min ≥ 2, 2 AZ, ELB health check
known schedule / every day at 9 AMScheduled scaling
recurring pattern, proactivelyPredictive scaling
randomly logged out, session lostElastiCache / DynamoDB session store (không phải stickiness)
in-flight requests dropped during scale inDeregistration delay (connection draining), lifecycle hook

Tự kiểm tra

Vì sao ASG nên dùng --health-check-type ELB thay vì mặc định EC2?

EC2 status check chỉ biết máy còn chạy. ELB health check gọi /actuator/health, phát hiện app treo, OOM, mất DB dù máy vẫn sống, và ASG mới thay được instance đó.

ALB tính tiền theo gì? Lab xong phải xóa thứ gì đầu tiên?

Theo giờ tồn tại + LCU, không phụ thuộc traffic; Free Tier chỉ 750 giờ trong 12 tháng đầu. Xóa ASG (về 0) rồi ALB; nhớ cả NAT Gateway.

Instance của ASG cứ launch rồi terminate sau 2 phút. Nguyên nhân hay gặp và lệnh nào để xem lý do?

Health check fail trong grace period quá ngắn (user data cài Java + boot Spring quá 120s), hoặc Actuator bị Security chặn trả 401, SG chưa mở 8080 từ ALB SG. Xem aws autoscaling describe-scaling-activities và aws elbv2 describe-target-health.

Deregistration delay và graceful shutdown của Spring Boot liên quan nhau thế nào?

Khi scale in, ALB ngừng gửi request mới và chờ deregistration delay (ví dụ 30s) cho request đang chạy; app nhận SIGTERM, graceful shutdown chờ ≤ timeout-per-shutdown-phase (đặt nhỏ hơn 30s) để xử lý xong rồi tắt. Thiếu một trong hai là có 502.

Phân biệt HTTPCode_Target_5XX và HTTPCode_ELB_5XX.

Target_5XX là lỗi app trả về (bug, DB). ELB_5XX là ALB tự sinh: 502 (target đóng kết nối / keep-alive lệch), 503 (không còn target healthy), 504 (quá idle timeout). Debug 502 lẻ tẻ: so idle timeout ALB với keep-alive của Tomcat, kiểm tra graceful shutdown.

Khi nào chọn NLB thay ALB?

Cần IP tĩnh/Elastic IP để whitelist, giao thức TCP/UDP không phải HTTP, latency cực thấp và hàng triệu kết nối, hoặc làm endpoint cho PrivateLink. ALB có thể là target của NLB khi cần cả hai.

🧪 Thực hành bài này: Intermediate: Ứng dụng HA: ALB + ASG + RDS + S3 + SQS (chưa triển khai) · lab labs/aws