Tháng 11/2025, một developer update AMI template, vô tình bỏ tick “delete storage volumes on termination”. Hai tháng sau, bill AWS từ $1,000/tháng vọt lên $40,000 – hàng trăm EBS volume 100GB mồ côi tích tụ. AWS chỉ miễn 40%.
Cùng năm, một startup bị DDoS vào ASG không set max capacity. Kết quả: 2,000 instance m5.24xlarge spin up trong 72 giờ, bill $120,000. Một team khác deploy Lambda Friday evening, trigger S3 event set * thay vì prefix cụ thể, Lambda tự trigger chính nó qua log file – $47,283 trong một weekend.
Tháng 3/2026, một AWS Community Builder dùng Bedrock qua Project Mantle gặp lỗi billing: raw token count bị tính như 1K-token billing units, $46 thực tế thành $46,337. Billing data mất 7 ngày mới hiển thị trong Cost Explorer – nhảy từ $140 lên $56,000 trong một đêm, bypass hoàn toàn $100 budget alarm mà tác giả đã cấu hình.
Điểm chung của tất cả những incident này: không có billing alarm đa lớp, không có anomaly detection, không có kênh alert đa dạng.
Bài này setup 3 lớp bảo vệ cost cho AWS account – dựa trên chính những gì các team trên đã ước họ làm trước khi incident xảy ra.
flowchart TB
subgraph "3-Layer Cost Protection"
L1["Layer 1: CloudWatch Billing Alarms<br/>Reactive · Threshold-based · 4 mức leo thang"]
L2["Layer 2: AWS Budgets<br/>Proactive · Forecasted alerts · Budget Actions"]
L3["Layer 3: Cost Anomaly Detection<br/>ML-driven · Pattern-based · Tự phát hiện spike"]
end
subgraph "Security Foundation"
Root["Root User<br/>Hardware MFA (2 YubiKeys)"]
IAM["IAM Admin<br/>Virtual MFA · dùng hàng ngày"]
end
SNS["SNS Fan-out → Email + Slack + PagerDuty"]
L1 --> SNS
L2 --> SNS
L3 --> SNS
Root -.->|"tạo + khóa"| IAM
```text
---
## Root user: chìa khóa vạn năng -- khóa nó lại
Root user là email + password bạn đăng ký AWS. Nó có quyền **vô hạn và không thể bị giới hạn**: xóa account, đóng billing, thay đổi support plan. **Không IAM policy nào, không SCP nào có thể chặn root** -- đây là design cố ý của AWS để đảm bảo bạn không bao giờ bị lock out hoàn toàn.
Chính vì vậy, root user là **mục tiêu số một của attacker**. RubyGems.org tháng 9/2025: attacker giành được root password, trong 4 phút enumerate toàn bộ IAM roles, lấy credential. Phát hiện 11 ngày sau vì không có CloudTrail alert cho root activity.
### MFA: 4 loại, 1 lựa chọn đúng cho root
| Loại MFA | Cơ chế | Chống phishing? | Dùng cho |
|----------|--------|-----------------|----------|
| **FIDO2 Security Key** (YubiKey, Google Titan) | Cryptographic challenge-response, verify origin website | **Có** -- key từ chối ký nếu domain không khớp | **Root user** |
| Hardware TOTP Token (Gemalto) | PIN + OTP hiển thị trên màn hình key fob | Không | Compliance (PCI-DSS yêu cầu hardware token) |
| Virtual MFA (Google Authenticator, Authy, 1Password) | TOTP 6 số, 30s rotation | Không -- OTP có thể bị phishing | IAM users hàng ngày |
| SMS MFA | OTP qua tin nhắn | Không -- dễ bị SIM-swap | **Đừng dùng cho ai cả** |
**Tại sao FIDO2 security key mạnh hơn virtual MFA?**
Virtual MFA sinh OTP 6 số mỗi 30 giây. Nếu attacker tạo trang login AWS giả, bạn nhập OTP vào trang giả → attacker có 30 giây để dùng OTP đó login vào AWS thật. Đây gọi là **real-time phishing attack**.
FIDO2 security key ký cryptographic challenge từ browser. Chữ ký này bao gồm **origin domain** (ví dụ `https://signin.aws.amazon.com`). Nếu bạn đang ở trang giả (`https://signin.aws-amazon.com`), key sẽ từ chối ký vì domain không khớp. Không có OTP nào để phishing.
### Quy trình setup root user
```bash
# 1. Tạo AWS account → nhận email verify + phone verify + credit card
# 2. Login root → IAM → Security credentials → Assign MFA device
# Chọn "Security key" → cắm YubiKey → chạm để đăng ký
# Làm lại với key backup thứ 2
# 3. Xóa tất cả access key của root (nếu lỡ tạo)
# Console: IAM → Security credentials → Root user → Access keys → Delete
# Không có CLI -- root user không phải IAM user, không dùng --user-name root được
# 4. Dùng password generator 20+ ký tự
# Lưu trong password manager (1Password / Bitwarden)
# 5. Tạo IAM admin user -- từ giờ chỉ dùng user này
aws iam create-user --user-name admin
aws iam attach-user-policy \
--user-name admin \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-login-profile \
--user-name admin \
--password 'Generated20CharPass!' \
--password-reset-required
# 6. Enable virtual MFA cho IAM admin
# Console → IAM → Users → admin → Security credentials → Assign MFA device
# Chọn "Virtual MFA" → quét QR bằng Authy/1Password
# 7. Logout root → login bằng IAM admin
# Từ giờ KHÔNG BAO GIỜ dùng root, trừ 4 exceptions bên dưới
```text
Root user **chỉ dùng cho 4 việc** (vài tháng hoặc vài năm một lần):
- Thay đổi billing/payment method
- Đóng AWS account
- Enable MFA Delete trên S3 bucket (cần root + MFA)
- Đăng ký AWS Support plan mới
### Break-glass procedure -- khi mất cả 2 YubiKey
```text
Tình huống: YubiKey chính bị mất, key backup trong safe ngân hàng (tối thứ 7)
→ Cần root access gấp để respond incident
Quy trình khẩn:
1. Gọi AWS Support (cần: credit card gốc + email gốc + photo ID)
2. AWS verify identity qua phone/video call (2-24 giờ)
3. AWS tạm thời disable MFA trên root
4. Login root → đăng ký MFA device mới NGAY LẬP TỨC
5. Review CloudTrail: có activity lạ trong thời gian mất key không?
Phòng tránh:
- 2 hardware key (primary + backup trong safe)
- 1 virtual MFA backup (điện thoại cũ, chỉ cài Authy, để trong safe)
- In backup code ra giấy, cất trong safe
```text
Tuyệt đối không tạo access key cho root user. Nếu key lộ trên GitHub (chuyện xảy ra mỗi ngày), attacker có toàn quyền account – không IAM policy nào chặn được root access key. IAM admin user cũng mạnh tương đương, nhưng ít nhất bạn có thể xóa nó, rotate key, hoặc restrict bằng SCP – còn root thì không.
---
## Layer 1: CloudWatch Billing Alarm -- cái phao $0
Billing alarm miễn phí (10 alarm metrics trong Free Tier), mất 5 phút setup. Là thứ **rẻ nhất và ROI cao nhất** trên AWS.
### Tại sao billing alarm chỉ hoạt động ở us-east-1?
AWS billing data được aggregate từ tất cả region về một central location. `AWS/Billing` CloudWatch namespace **chỉ tồn tại ở us-east-1**. Đây là AWS design decision từ khi CloudWatch mới ra mắt -- billing là global data, nhưng metric infrastructure lúc đó chỉ support single-region namespace. Đến nay vẫn vậy.
Hệ quả: mọi billing alarm, budget, Cost Explorer setup đều phải thực hiện với `--region us-east-1`.
### Billing data delay -- cạm bẫy 6-8 giờ
`EstimatedCharges` metric KHÔNG real-time. Nó cập nhật mỗi **6-8 giờ**. Điều này có nghĩa:
- Bạn có thể đã tiêu $5,000 trong 3 giờ nhưng alarm chưa kêu
- Bedrock billing bug $46K: mất **7 ngày** data mới xuất hiện trong Cost Explorer
- Đây là lý do bạn cần **3 lớp bảo vệ**, không chỉ billing alarm
### Setup đầy đủ: 4 mức alarm + SNS fan-out
```bash
# ===== BƯỚC 1: Enable billing alerts =====
# Console: Account → Billing Preferences → Receive CloudWatch Billing Alerts
# Không có CLI tương đương -- phải dùng Console
# ===== BƯỚC 2: Tạo SNS topic cho alert =====
TOPIC_ARN=$(aws sns create-topic \
--name billing-critical \
--region us-east-1 \
--query 'TopicArn' --output text)
# Subscribe email
aws sns subscribe \
--topic-arn $TOPIC_ARN \
--protocol email \
--region us-east-1 \
--notification-endpoint [email protected]
# Subscribe Lambda (→ Slack #aws-cost)
aws sns subscribe \
--topic-arn $TOPIC_ARN \
--protocol lambda \
--region us-east-1 \
--notification-endpoint arn:aws:lambda:us-east-1:123456789012:function:cost-to-slack
# ===== BƯỚC 3: 4 mức alarm leo thang =====
MONTHLY_BUDGET=500
# Mức 1: 25% -- "FYI, đang tiêu bình thường"
aws cloudwatch put-metric-alarm --region us-east-1 \
--alarm-name "billing-25pct-info" \
--metric-name EstimatedCharges --namespace AWS/Billing \
--statistic Maximum --period 21600 --evaluation-periods 1 \
--threshold $((MONTHLY_BUDGET * 25 / 100)) \
--comparison-operator GreaterThanThreshold \
--dimensions Name=Currency,Value=USD \
--alarm-actions $TOPIC_ARN
# Mức 2: 50% -- "Cần chú ý, kiểm tra Cost Explorer"
aws cloudwatch put-metric-alarm --region us-east-1 \
--alarm-name "billing-50pct-warning" \
--metric-name EstimatedCharges --namespace AWS/Billing \
--statistic Maximum --period 21600 --evaluation-periods 1 \
--threshold $((MONTHLY_BUDGET * 50 / 100)) \
--comparison-operator GreaterThanThreshold \
--dimensions Name=Currency,Value=USD \
--alarm-actions $TOPIC_ARN
# Mức 3: 80% -- "Nguy hiểm, kiểm tra ngay!"
aws cloudwatch put-metric-alarm --region us-east-1 \
--alarm-name "billing-80pct-critical" \
--metric-name EstimatedCharges --namespace AWS/Billing \
--statistic Maximum --period 21600 --evaluation-periods 1 \
--threshold $((MONTHLY_BUDGET * 80 / 100)) \
--comparison-operator GreaterThanThreshold \
--dimensions Name=Currency,Value=USD \
--alarm-actions $TOPIC_ARN
# Mức 4: 100% -- "Vượt ngân sách! Hành động khẩn!"
aws cloudwatch put-metric-alarm --region us-east-1 \
--alarm-name "billing-100pct-emergency" \
--metric-name EstimatedCharges --namespace AWS/Billing \
--statistic Maximum --period 21600 --evaluation-periods 1 \
--threshold $MONTHLY_BUDGET \
--comparison-operator GreaterThanThreshold \
--dimensions Name=Currency,Value=USD \
--alarm-actions $TOPIC_ARN
```text
### Per-service alarms: biết chính xác service nào đốt tiền
Tổng bill $500 không nói lên điều gì. Per-service alarm cho biết **EC2 hay NAT Gateway đang là thủ phạm**:
```bash
# EC2-specific
aws cloudwatch put-metric-alarm --region us-east-1 \
--alarm-name "EC2-Bill-Exceeds-200" \
--metric-name EstimatedCharges --namespace AWS/Billing \
--statistic Maximum --period 21600 --evaluation-periods 1 \
--threshold 200 --comparison-operator GreaterThanThreshold \
--dimensions Name=ServiceName,Value=AmazonEC2 Name=Currency,Value=USD \
--alarm-actions $TOPIC_ARN
# Tương tự: AmazonRDS, AmazonS3, AWSLambda, AmazonDynamoDB, AWSDataTransfer, AmazonCloudWatch
# NATGateway, AmazonSageMaker, AmazonBedrock -- những service ĐẮT nhất
```text
### Giải thích các parameter
- `--period 21600` (6 giờ = 21,600 giây): Billing data cập nhật mỗi ~6 giờ, poll nhanh hơn vô ích, chỉ tốn cost
- `--statistic Maximum`: `EstimatedCharges` là cumulative metric -- nó chỉ tăng trong billing cycle, không giảm. Maximum = giá trị mới nhất (current running total)
- `--evaluation-periods 1`: Alarm kích hoạt sau 1 datapoint vượt threshold (không cần chờ confirm)
- `--dimensions Name=Currency,Value=USD`: Bắt buộc -- mọi billing metric đều là USD
### SNS fan-out: đừng chỉ gửi email
Email alert dễ bị ignore (100+ email/ngày, alert chìm trong inbox). Pattern đúng: **SNS fan-out đến nhiều channel với severity khác nhau**:
```text
SNS Topic (billing-critical)
├── Lambda → Slack #aws-cost-alerts (cả team thấy)
├── AWS Chatbot → Microsoft Teams
├── Lambda → PagerDuty (critical alert → page oncall)
└── Email subscription (backup, finance team)
```text
```typescript
// Lambda: forward billing alert → Slack
import { SNSHandler } from "aws-lambda";
import { WebClient } from "@slack/web-api";
const slack = new WebClient(process.env.SLACK_BOT_TOKEN!);
export const handler: SNSHandler = async (event) => {
const message = JSON.parse(event.Records[0].Sns.Message);
const newState = message.NewStateValue; // "ALARM" hoặc "OK"
if (newState === "ALARM") {
await slack.chat.postMessage({
channel: "#aws-cost-alerts",
text: [
`:rotating_light: *AWS Billing Alert: $${message.NewStateReason}*`,
`Threshold: ${message.Trigger.MetricName}`,
`Time: ${message.StateChangeTime}`,
`Dashboard: <https://console.aws.amazon.com/cloudwatch/...|CloudWatch>`,
].join("\n"),
});
}
};
```text
---
## Layer 2: AWS Budgets -- proactive, không reactive
CloudWatch alarm chỉ nhìn **actual spend**. Budgets nhìn **forecasted spend** -- dự đoán cuối tháng bạn sẽ tiêu bao nhiêu dựa trên run rate hiện tại.
**Forecasted alert là tính năng quan trọng nhất** -- nó có thể kích hoạt vào ngày 10 của tháng (khi bạn mới tiêu 30% budget nhưng run rate dự đoán sẽ vượt), cho bạn 20 ngày để điều chỉnh.
```bash
aws budgets create-budget \
--account-id 123456789012 \
--budget '{
"BudgetName": "monthly-500-usd",
"BudgetLimit": {"Amount": "500", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}' \
--notifications-with-subscribers '[
{
"Notification": {
"NotificationType": "FORECASTED",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{"SubscriptionType": "EMAIL", "Address": "[email protected]"},
{"SubscriptionType": "SNS", "Address": "'$TOPIC_ARN'"}
]
},
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{"SubscriptionType": "EMAIL", "Address": "[email protected]"}
]
}
]'
```text
### Budget Actions: tự động phản ứng
```bash
# Khi forecast vượt 100% budget → tự động deny tạo resource mới
aws budgets create-budget-action \
--budget-name monthly-500-usd \
--action-type APPLY_IAM_POLICY \
--action-threshold '{"Value": 100, "Type": "PERCENTAGE"}' \
--definition '{
"PolicyArn": "arn:aws:iam::123456789012:policy/deny-new-resources"
}' \
--execution-role-arn arn:aws:iam::123456789012:role/budget-action-execution \
--notification-type FORECASTED
```text
Budget Actions mạnh tay cho non-production, thận trọng cho production. Dev account: auto-stop EC2 instances, deny tạo resource mới khi vượt budget 100%. Production: chỉ gửi alert, không tự động action – bạn không muốn budget action tắt production app lúc 3h sáng.
---
## Layer 3: Cost Anomaly Detection -- ML phát hiện spike
Fixed threshold (25%, 50%, 80%) không phát hiện được **spike bất thường**. Cost Anomaly Detection dùng ML để phát hiện pattern lạ so với baseline lịch sử:
```bash
aws ce create-anomaly-monitor \
--anomaly-monitor '{
"MonitorName": "daily-cost-spike",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE"
}'
aws ce create-anomaly-subscription \
--anomaly-subscription '{
"SubscriptionName": "alert-on-spike",
"Frequency": "IMMEDIATE",
"Threshold": 100,
"Subscribers": [
{"Type": "EMAIL", "Address": "[email protected]"},
{"Type": "SNS", "Address": "'$TOPIC_ARN'"}
]
}'
```text
**Hạn chế:** Cần ~10 ngày data cho service/account mới. Trong 2 tuần đầu sau khi mở account hoặc launch service mới, dựa vào Budgets + CloudWatch alarms.
---
## Cost Explorer: drill-down khi alarm kêu
Mất ~24 giờ sau khi enable mới có data đầu tiên.
```bash
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-06-21 \
--granularity DAILY \
--metrics "UnblendedCost" \
--group-by Type=DIMENSION,Key=SERVICE \
--filter '{"Dimensions": {"Key": "REGION", "Values": ["ap-southeast-1"]}}'
```text
```mermaid
flowchart TD
Bill["Charges tháng này: $580"]
EC2["EC2: $210<br/>g4dn.xlarge quên tắt"]
NAT["NAT Gateway: $96<br/>3 cái × $32/tháng"]
S3["S3: $134<br/>bucket public bị leech"]
RDS["RDS: $95<br/>db.r5.large thay vì t4g"]
Lambda["Lambda: $5"]
Other["Khác: $40"]
Bill --> EC2
Bill --> NAT
Bill --> S3
Bill --> RDS
Bill --> Lambda
Bill --> Other
```text
---
## Free Tier: thay đổi lớn từ 07/2025
**Tài khoản tạo từ 15/07/2025:** Credit-based. **$100 upfront** tự động + kiếm thêm tối đa **$100** qua 5 guided activities (launch EC2, configure RDS, build Lambda web app, submit Bedrock prompt, set up Budgets). Tổng tối đa **$200 credits**.
**Tài khoản tạo trước 15/07/2025:** Giữ nguyên 3-tier cũ:
| Loại | Thời hạn | Ví dụ quan trọng |
|------|----------|-----------------|
| **Always Free** | Không giới hạn | Lambda 1M requests/tháng, DynamoDB 25GB, S3 5GB, CloudWatch 10 metrics |
| **12-Month Free** | 12 tháng từ khi tạo | EC2 t2.micro 750h/tháng, RDS db.t2.micro 750h/tháng, 30GB EBS |
| **Short-Term Trial** | 30-90 ngày | SageMaker 250h, Redshift 750h/tháng (2 tháng), GuardDuty 30 ngày |
### 3 cái bẫy Free Tier
1. **NAT Gateway KHÔNG trong Free Tier** -- $32/tháng. Nhiều người nghĩ "Free Tier" rồi bật NAT Gateway → ngạc nhiên bill $32
2. **EC2 t2.micro hết 12 tháng → tự động charge on-demand** (~$72/tháng nếu chạy 24/7). **Không có cảnh báo trước**
3. **RDS backup storage vượt allocated** → charge thêm. Free Tier cho 20GB backup, DB 30GB → backup vượt
---
## Checklist 30 phút cho account mới
```mermaid
flowchart LR
subgraph "1-5"
A1["Tạo account"] --> A2["Hardware MFA root"]
end
subgraph "5-10"
A2 --> B1["Xóa access key root"]
B1 --> B2["Tạo IAM admin + MFA"]
end
subgraph "10-20"
B2 --> C1["Enable billing alerts"]
C1 --> C2["SNS topic + fan-out"]
C2 --> C3["4 mức alarm"]
end
subgraph "20-30"
C3 --> D1["Budget + forecasted alert"]
D1 --> D2["Cost Explorer + Anomaly Detection"]
end
```text
1. Tạo account, verify phone + credit card
2. **Hardware MFA cho root** (2 YubiKeys -- 1 backup)
3. Password 20+ ký tự → password manager
4. **Xóa tất cả access key của root**
5. Tạo IAM admin → `AdministratorAccess` → virtual MFA
6. **Logout root, login IAM admin**
7. Region → **us-east-1**
8. Enable billing alerts
9. SNS topic → email + Lambda (→ Slack)
10. 4 mức billing alarm: 25%, 50%, 80%, 100%
11. Per-service alarms: EC2, RDS, Lambda, NAT GW
12. Budget + FORECASTED 80% alert
13. Cost Explorer + Cost Anomaly Detection
14. Calendar reminder: Free Tier hết hạn (12 tháng)
---
- **3 lớp bảo vệ cost**: CloudWatch Billing Alarms (threshold) + AWS Budgets (forecast) + Cost Anomaly Detection (ML spike detection). Mỗi lớp bắt một kiểu failure khác nhau
- **Root user = hardware MFA (2 keys), không access key, không dùng hàng ngày**. IAM admin + virtual MFA cho mọi việc
- **Billing alarm 4 mức (25/50/80/100%)** -- mỗi mức route đến channel khác nhau (email, Slack, PagerDuty)
- **Billing data delay 6-8 giờ** (có thể lên đến 7 ngày trong edge case) -- đừng rely vào một lớp duy nhất
- **Free Tier thay đổi 07/2025** -- credit-based cho tài khoản mới. Đánh dấu calendar 12 tháng nếu dùng 3-tier cũ
Bài sau: [Phần 2: IAM user, group, role, policy -- least privilege](/posts/aws/02-iam-user-group-role-policy/)
## Câu hỏi hay gặp
**Q: Tôi có cần hardware MFA (YubiKey $50) hay virtual MFA đủ?**
A: Với root user, hardware MFA là **best practice 2025-2026** vì chống phishing. YubiKey giá $25-50, rẻ hơn 1 phút downtime production. Với IAM user hàng ngày, virtual MFA là đủ.
**Q: Billing alarm có guarantee tôi không bị bill shock không?**
A: **Không.** Billing data delay 6-8 giờ, edge case lên đến 7 ngày (Bedrock Mantle incident). Billing alarm là lớp 1. Cần Budgets (lớp 2) + Anomaly Detection (lớp 3) + per-service alarm.
**Q: Cost Anomaly Detection khác gì Budget forecast?**
A: Budget forecast dựa trên **run rate tuyến tính** (tiêu $X/ngày × 30 ngày). Anomaly Detection dùng **ML pattern matching** -- phát hiện spike đột ngột mà run rate không dự đoán được (DDoS → 2,000 instances trong 3 giờ).