Mười quyết định
Tick từng dòng khi bạn đồng ý. Mọi thứ phía dưới đều bắt nguồn từ mười lựa chọn này.
-
D1 Step Functions Standard cho escalation
Wait state có sẵn, retry có sẵn, và xem được từng execution dạng đồ thị — đúng yêu cầu "dễ trace". Đắt hơn cron sweeper khoảng 1 USD/tháng.
-
D2API Gateway HTTP API
1 USD/triệu request, có throttle và custom domain. Rẻ hơn REST API bốn lần.
-
D3DynamoDB single-table, on-demand
Không có chi phí lúc rảnh. TTL miễn phí lo việc dọn dữ liệu cũ.
-
D4SQS FIFO, MessageGroupId = fingerprint
Giữ đúng thứ tự firing → resolved cho cùng một alert. Một triệu request đầu mỗi tháng miễn phí.
-
D5Config bằng YAML trong Git, không làm UI
Lịch trực, escalation policy, route đều nằm trong một file, apply bằng bananactl. Review qua PR, rollback bằng git revert. Đây là thứ cắt được nhiều effort nhất khỏi MVP.
-
D6Cognito + Google cho người, token cho webhook
Không phải viết dòng auth nào, miễn phí dưới 50k MAU. RBAC phase 2 chỉ cần map Cognito Group sang JWT claim.
-
D7Google Calendar qua secret iCal URL
Read-only, poll 5 phút, không cần OAuth flow. Grafana OnCall cũng làm đúng như vậy. Tiết kiệm cả tuần.
-
D8Telegram trước, iOS sau
Nút Ack ngay trong tin nhắn, miễn phí, làm xong trong 2–3 ngày. iOS app tốn 99 USD/năm và có rủi ro Apple từ chối entitlement.
-
D9SLA board trên Grafana on-prem sẵn có
CloudWatch EMF → CloudWatch datasource → Grafana Public Dashboard. Không tốn thêm đồng nào.
-
D10Hexagonal Go — chạy được cả Lambda lẫn RKE2
Core domain không import AWS SDK. Hai adapter cho mỗi port. Khoảng 150 dòng code thêm, đổi lấy việc không bị khoá vào AWS.
Tại sao là bây giờ
Grafana OnCall OSS đã bị archive ngày 2026-03-24. Cùng ngày đó, Cloud Connection — thứ cung cấp SMS, phone call và mobile push cho bản self-hosted — cũng bị tắt.
Nghĩa là không còn phương án "dùng tạm rồi tính sau".
| Hệ thống | Lấy gì | Bỏ gì |
|---|---|---|
| Grafana OnCall | Data model Integration → Route → Escalation Chain → Schedule. Alert grouping. Lịch trực đọc từ iCal. | Django + Celery + Redis + Postgres — nặng và tốn tiền lúc rảnh. Phụ thuộc Grafana Cloud để notify. |
| GoAlert | Model escalation policy → step → target rõ ràng. Single binary. Override shift thủ công. | Bắt buộc Twilio. Không có Telegram. Postgres phải chạy 24/7. |
| Alertmanager | Routing tree với label matcher, group_by, silence. Đây là design routing tốt nhất — copy thẳng. | Không biết escalate theo người và theo thời gian, không có ack. Chính là chỗ BananaOnCall bù vào. |
| PagerDuty | Khái niệm urgency, notification rule per-user, maintenance window, status page. | Tính tiền theo đầu người. Năm user đã hơn 100 USD/tháng. |
Làm gì và cố tình không làm gì
- G1Nhận webhook từ Alertmanager, Grafana Alerting, hoặc JSON tuỳ ý
- G2Gom nhóm và khử trùng lặp — một vấn đề là một alert, không spam
- G3Route theo label, escalation nhiều bước có timeout
- G4Tìm người đang trực từ Google Calendar
- G5Telegram kèm nút Ack, Resolve, Silence
- G6Timeline đầy đủ cho mọi alert
- G7SLA board công khai trên Grafana
- G8Hạ tầng bằng Terraform, config bằng YAML trong Git
- Web UI đầy đủ — dùng CLI, Grafana và Telegram thay thế
- iOS app — phase 3
- RBAC chi tiết — MVP chỉ có admin và responder
- Multi-tenant — nhưng data model đã chừa chỗ
- SMS và voice call — đắt, Telegram đủ dùng
- Incident management, war room, postmortem — phase 4
- Multi-region active-active — phase 4
- ML alert correlation — không bao giờ, cho MVP
Rủi ro lớn nhất của project này không phải kỹ thuật mà là scope creep. Danh sách bên phải chính là thứ giữ MVP về đích.
Functional requirements
Mỗi nhóm mở ra được. Cột bên phải là điều kiện nghiệm thu — không mơ hồ.
▸FR-1Nhận alert
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-1.1 | Webhook Alertmanager | Trả 202 trong dưới 500ms p95 |
| FR-1.2 | Webhook Grafana Alerting | Cùng contract |
| FR-1.3 | Webhook generic, map field bằng JSONPath | Test với payload tuỳ ý |
| FR-1.4 | Xác thực bằng integration key 32 byte trong path | Key sai trả 401, không lộ thông tin |
| FR-1.5 | Không mất alert khi downstream lỗi | Ghi SQS xong mới trả 202, có DLQ |
| FR-1.6 | Gửi lại cùng payload không tạo alert trùng | MessageDeduplicationId + conditional write |
▸FR-2Gom nhóm và khử trùng lặp
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-2.1 | Fingerprint = sha256 của integration + label trong group_by | 10 alert cùng fingerprint chỉ notify một lần |
| FR-2.2 | group_by cấu hình được, mặc định alertname, namespace, service | Đổi trong YAML, apply, có hiệu lực |
| FR-2.3 | Alert mới khi group đang mở thì gộp vào, không escalate lại | alert_count tăng, không có tin nhắn mới |
| FR-2.4 | Alert mới khi group đã đóng thì tạo group mới | |
| FR-2.5 | Alertmanager gửi resolved thì tự đóng group | Timeline ghi resolved_by system |
▸FR-3Routing và escalation
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-3.1 | Route theo label matcher, first-match-wins, có route mặc định | Route mặc định là bắt buộc |
| FR-3.2 | Escalation policy nhiều bước, mỗi bước có target và wait_after | |
| FR-3.3 | Target: người đang trực, một user cụ thể, hoặc một group chat | |
| FR-3.4 | Chưa ack sau wait_after thì sang bước tiếp | Không ack 5 phút, người thứ hai nhận trong 5m ± 15s |
| FR-3.5 | Ack hoặc resolve thì dừng escalation ngay | |
| FR-3.6 | Bước cuối lặp lại đến khi có người trả lời | Chống trường hợp cả team ngủ. Tối đa 50 lần |
| FR-3.7 | Escalation sống sót qua deploy và Lambda restart | State ở Step Functions và DynamoDB, không giữ trong memory |
▸FR-4Lịch trực
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-4.1 | Sync từ Google Calendar iCal mỗi 5 phút | Sửa lịch, 5 phút sau CLI trả về đúng người |
| FR-4.2 | Map event sang user qua tiêu đề oncall: tên | |
| FR-4.3 | Hỗ trợ event lặp lại theo RRULE | Test với rotation hàng tuần |
| FR-4.4 | Trả lời "ai đang trực lúc T" dưới 50ms | Materialize shift vào DB, không parse ICS lúc runtime |
| FR-4.5 | Override thủ công đè lên lịch | Một lệnh CLI |
| FR-4.6 | Không có ai trực thì fallback và báo admin | Bắt buộc — lịch trống hoặc sync lỗi là failure mode hay bị quên nhất |
| FR-4.7 | Lưu UTC, hiển thị theo Asia/Ho_Chi_Minh |
▸FR-5Thông báo và phản hồi
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-5.1 | Telegram DM với nút Ack, Resolve, Silence 1h, Runbook | Bấm Ack đổi state dưới 2 giây |
| FR-5.2 | User liên kết tài khoản bằng lệnh /link | |
| FR-5.3 | Telegram lỗi thì retry 3 lần, hết thì escalate ngay | Không chờ hết wait_after |
| FR-5.4 | Không gửi trùng khi hệ thống retry | dedupKey theo group, step, kênh, user |
| FR-5.5 | Sau khi ack thì sửa tin nhắn cũ thành "Acked by @X" | |
| FR-5.6 | Tin nhắn có đủ severity, summary, label, thời gian, link runbook và Grafana | Đọc là biết phải làm gì, không cần mở laptop |
| FR-6.3 | State machine chặt, transition sai trả 409 | Idempotent với at-least-once delivery |
| FR-6.5 | Mọi thay đổi ghi timeline: ai, làm gì, lúc nào, từ đâu |
▸FR-8SLA và tự giám sát
| ID | Yêu cầu | Nghiệm thu |
|---|---|---|
| FR-8.1 | Emit metric qua CloudWatch EMF cho mọi sự kiện | |
| FR-8.2 | Grafana board nội bộ: availability, latency, delivery, MTTA, MTTR | |
| FR-8.3 | Board công khai cho end user: uptime, error budget còn lại, lịch sử sự cố | Không cần đăng nhập |
| FR-8.4 | Dead-man switch chạy trên hạ tầng độc lập | Bắt buộc — BananaOnCall không thể tự báo động khi chính nó chết. Canary trên RKE2 ping mỗi phút, im lặng quá 3 phút thì bot Telegram thứ hai báo |
Bảy Lambda, không hơn
Một Lambda một trách nhiệm — nhưng không phải một Lambda cho mỗi endpoint. Đủ tách để trace, chưa đủ nhiều để thành distributed monolith.
┌──────────────────────────────────────────────────────────────────┐
│ NGUỒN — RKE2 cluster on-prem │
│ Alertmanager · Grafana Alerting · Elastic Watcher · CI │
└─────────────────────────────┬────────────────────────────────────┘
│ HTTPS webhook
▼
┌───────────────────────────┐
│ API Gateway HTTP API │ custom domain + throttle
└─────────────┬─────────────┘
┌────────────────────┬─┴──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ λ ingest │ │ λ api │ │ λ telegram-hook │
│ verify+parse │ │ ack/resolve │ │ callback_query │
└──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ SQS FIFO │ │
│ grp=fingerprint │ │
▼ │ │
┌──────────────┐ │ │
│ λ processor │ │ │
│ dedupe·group │ │ │
│ match route │ │ │
└──────┬───────┘ │ │
│ StartExecution ▼ ▼
▼ ┌───────────────────────────────────────┐
┌───────────────┐ │ DynamoDB │
│ Step Functions│◄┤ single-table · GSI1 · TTL │
│ Notify→Wait │─►│ AlertGroup·Shift·User·Policy·Route │
│ →Check→Choice │ └───────────────────────────────────────┘
└──────┬────────┘
▼
┌──────────────┐ ┌────────────────────────┐
│ λ notifier │───────►│ Telegram Bot API │
│ ai đang trực │ │ phase 3: APNs │
└──────────────┘ └────────────────────────┘
EventBridge Scheduler
├─ rate(5m) → λ schedule-sync ──► Google Calendar iCal
├─ rate(1h) → λ sla-rollup ──► DynamoDB rollup
└─ rate(1m) → λ healthcheck ──► dead-man switch
Mọi λ ──EMF──► CloudWatch ──► Grafana on-prem ──► Public board
Escalation engine — ba phương án
| A · Step Functions | B · Cron sweeper 1 phút | C · EventBridge one-time | |
|---|---|---|---|
| Độ chính xác | ±1s | ±60s | ±1s |
| Trace và debug | Đồ thị execution có sẵn | Phải đọc log | Phải đọc log |
| Retry | Built-in | Tự code | Tự code |
| Code phải viết | Ít nhất | Nhiều nhất — cần distributed lock | Trung bình |
| Chi phí / tháng | ~1,03 USD | ~0,10 USD | ~0,05 USD |
| Chạy ngoài AWS | Không | Được | Không |
Chọn A. Chênh 1 USD/tháng không đáng so với hàng chục giờ debug. Nhược điểm khoá vào AWS được xử lý bằng interface EscalationEngine với hai implementation: sfn cho Lambda và ticker cho khi chạy trên k8s.
Từ alert đến ack
-
T+0sAlertmanager POST tới /v1/int/{key}/alertmanager
-
T+0.1s · λ ingestVerify key, normalize, đẩy vào SQS FIFO, trả 202. Không chạm database.
-
T+0.5s · λ processorTính fingerprint. Tra dedupe pointer bằng một GetItem, không scan. Chưa có group thì tạo mới, match route, gọi StartExecution.
-
T+1.5s · λ notifierTra ai đang trực, lấy chat_id, gửi tin nhắn kèm bốn nút. Ghi lại provider message id để sau này sửa được.
-
T+2m · người trực bấm AckTelegram gọi webhook. Verify secret token. UpdateItem có ConditionExpression state = firing nên bấm hai lần cũng không sao. Sửa tin nhắn cũ, ghi timeline, emit time_to_ack_seconds.
-
T+6m30s · Step Functions kiểm traState đã là acked → workflow kết thúc, không escalate. Nếu vẫn firing thì sang người thứ hai.
-
T+25m · Alertmanager gửi resolvedGroup đóng, TTL 90 ngày bắt đầu đếm, emit time_to_resolve_seconds.
Một bảng, sáu access pattern
Không có query nào phải scan. Pattern quan trọng nhất — tra xem fingerprint này đã có alert đang mở chưa — tốn đúng một GetItem.
| Entity | pk | sk | Ghi chú |
|---|---|---|---|
| AlertGroup | AG#<ulid> | META | GSI1 theo state để list alert đang firing |
| Alert thô | AG#<ulid> | ALERT#<ts> | TTL 90 ngày |
| Timeline | AG#<ulid> | LOG#<ts> | Một query lấy hết alert kèm lịch sử |
| Dedupe pointer | FP#<int>#<fp> | OPEN | Conditional put chống race hai alert đến cùng lúc |
| Escalation policy | EP#<id> | STEP#<order> | |
| Shift | SCHED#<id> | SHIFT#<startISO> | Query ngược, limit 1 để biết ai đang trực |
| Contact | USER#<id> | CONTACT#telegram | GSI1 theo chat_id để tra ngược từ Telegram |
| SLA rollup | SLO#<sli> | DAY#<date> |
Hai alert cùng fingerprint đến đồng thời sẽ cùng tạo group. SQS FIFO với MessageGroupId bằng fingerprint đã serialize chúng, nhưng vẫn phải có ConditionExpression trên dedupe pointer. Ai thắng thì tạo group, ai thua thì đọc group_id rồi gộp vào.
99,9% nghĩa là gì
Error budget = 43 phút 49 giây mỗi tháng
| SLI | Định nghĩa | Target |
|---|---|---|
| Ingest availability | Tỉ lệ request vào endpoint webhook không trả 5xx, cửa sổ 28 ngày | 99,9% |
| Notification latency | Tỉ lệ alert có tin nhắn đầu tiên gửi đi trong vòng 30 giây | 99% |
| Delivery success | Tỉ lệ tin nhắn được provider xác nhận đã nhận | 99,5% |
| Escalation correctness | Tỉ lệ bước escalation chạy đúng hạn trong sai số 30 giây | 99% |
Serverless một region thực tế đạt khoảng 99,95% ở tầng ứng dụng, nhưng một sự cố region-wide của AWS sẽ ăn hết error budget của cả quý. Đề xuất MVP cam kết 99,9% đo tại tầng ứng dụng và ghi rõ loại trừ region outage trong văn bản SLA — thay vì hứa 99,9% tuyệt đối rồi buộc phải làm multi-region ngay từ đầu.
Khoảng 8 USD một tháng
Giả định 3.000 alert, 10.000 tin nhắn, 150.000 request, 5 người, một region Singapore. Cần verify lại trên AWS Pricing Calculator trước khi chốt ngân sách.
| Dịch vụ | Cách tính | USD/tháng |
|---|---|---|
| Lambda | ~200k invoke, phần lớn trong free tier | 0,30 |
| API Gateway HTTP | 150k request | 0,15 |
| DynamoDB on-demand | ~500k đọc, 200k ghi | 0,50 |
| SQS FIFO | Trong free tier | 0,00 |
| Step Functions | 45k state transition, 4k đầu miễn phí | 1,03 |
| EventBridge Scheduler | ~45k invoke | 0,05 |
| CloudWatch Logs | ~2GB ingest | 1,20 |
| CloudWatch Metrics | 15 custom metric — khoản lớn nhất, hơi bất ngờ | 4,50 |
| SSM · Cognito · ACM · Telegram | Free tier | 0,00 |
| Route 53 | Một hosted zone | 0,50 |
| Grafana | Dùng instance on-prem sẵn có | 0,00 |
| Tổng | 8,25 |
Remote-write metric thẳng vào Prometheus on-prem qua VPN thay vì dùng CloudWatch custom metric — tiết kiệm 4,50 USD, tức hơn một nửa hoá đơn. Đổi lại phải mở đường mạng từ Lambda về on-prem.
PagerDuty 5 người khoảng 105–210 USD/tháng. Grafana Cloud IRM khoảng 100 USD. BananaOnCall rẻ hơn 15 đến 25 lần, và chi phí tăng theo lượng alert chứ không theo số người.
Bốn giai đoạn
Repo, CI, Terraform backend, package domain thuần với unit test đầy đủ cho state machine và fingerprint. Adapter in-memory để chạy local.
Xong khi go run ./cmd/server nhận được webhook và in ra console.
Tuần 1: DynamoDB, ingest, processor, dedupe. Tuần 2: Step Functions, notifier, Telegram, ack. Tuần 3: sync lịch, CLI apply, Terraform prod, test end-to-end.
- 1Alertmanager thật trên RKE2 fire alert, nhận Telegram dưới 15 giây
- 2Mười alert cùng fingerprint chỉ ra một tin nhắn
- 3Không ack 5 phút, người thứ hai nhận được
- 4Bấm Ack, escalation dừng và tin nhắn được sửa
- 5Alertmanager gửi resolved, group tự đóng
- 6Sửa Google Calendar, 5 phút sau CLI trả về đúng người
- 7terraform destroy rồi apply, hệ thống chạy lại từ đầu
Metric đầy đủ, rollup SLA, hai Grafana board, dead-man switch, silence và maintenance window, Cognito với ba role, CLI hoàn chỉnh.
SwiftUI, APNs, ack ngay từ notification, phân phối qua TestFlight.
Muốn thông báo xuyên qua chế độ im lặng thì cần Critical Alerts entitlement — phải nộp đơn xin Apple duyệt thủ công và Apple từ chối khá nhiều. Phương án an toàn là Time Sensitive: tự bật được, xuyên qua Focus mode nhưng không xuyên qua nút gạt im lặng. Cộng thêm 99 USD/năm tài khoản developer.
Region thứ hai với Route 53 failover, thêm kênh Slack và email, web UI nếu thực sự cần, multi-tenant, inhibit rule.
Cái gì có thể hỏng
| Mức độ | Rủi ro | Cách xử lý |
|---|---|---|
| CRITICAL | BananaOnCall chết mà không ai biết | Dead-man switch bắt buộc ở phase 2. Canary chạy trên RKE2 — hạ tầng hoàn toàn độc lập — ping mỗi phút, im lặng quá 3 phút thì bot Telegram thứ hai báo trực tiếp. |
| HIGH | Scope creep | Danh sách không-làm ở mục 03 là hợp đồng. Mọi thứ ngoài đó đẩy sang phase 2 trở đi. |
| HIGH | Telegram bị chặn hoặc API sập | Provider interface đã trừu tượng sẵn. Phase 2 thêm email qua SES làm kênh dự phòng ở bước cuối. |
| HIGH | AWS region outage | Chấp nhận ở MVP, ghi rõ trong SLA. Phase 4 làm region thứ hai. |
| MEDIUM | Sync Google Calendar lỗi | Không xoá shift cũ khi fetch thất bại. Có default target. Fail ba lần liên tiếp thì báo admin. |
| MEDIUM | Alert storm nghìn alert một phút | SQS làm bộ đệm, Lambda reserved concurrency, grouping cắt phần lớn. Phase 2 thêm rate limit theo integration. |
| MEDIUM | Execution history của Step Functions đầy | Giới hạn repeat 50 lần rồi kết thúc và báo admin. |
Mười câu hỏi chặn
Ba câu đầu chặn nhiều nhất — trả lời được ba câu đó là bắt đầu code phase 0 được.
- Q1Step Functions hay cron sweeper? Chọn sweeper nếu bạn ưu tiên tối đa khả năng port sang RKE2.
- Q2Hiện có bao nhiêu người trong rotation? Có primary và secondary hay chỉ một tầng?
- Q3Board công khai report uptime của BananaOnCall, của các service nó giám sát, hay cả hai?
- Q4Domain cho endpoint?
- Q5Region ap-southeast-1 Singapore — xác nhận?
- Q6Dùng AWS account có sẵn hay tạo account riêng cho project?
- Q7Nhịp escalation 5 phút, 5 phút, war room, lặp 10 phút — hợp với team bạn chưa?
- Q8Quy ước Google Calendar: tiêu đề oncall: tên hay dùng email người tham dự?
- Q9Public repo ngay hay private trước? Ảnh hưởng tới cách quản lý secret.
- Q10Có cần đẩy alert vào Elastic hoặc Kafka sẵn có để lưu trữ dài hạn không?