Design doc v0.1 2026-08-22 Chờ duyệt

BananaOnCall

On-call engine chạy trên AWS Serverless. Nhận alert từ Alertmanager, gom nhóm, tìm đúng người đang trực trên Google Calendar, bắn Telegram kèm nút Ack — và tự escalate nếu không ai trả lời.

Escalation clock · ep-critical
T+0s
Primary
Telegram DM tới người đang trực
T+5m
Secondary
Chưa ack → người trực dự phòng
T+10m
War room
Cả team trong group chat
Lặp lại
Mỗi 10m
Tối đa 50 lần rồi báo admin
FIRING ACKED RESOLVED SILENCED Go · Terraform · DynamoDB · Apache-2.0
01 — Duyệt trước khi code

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.

  1. 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.

  2. D2API Gateway HTTP API

    1 USD/triệu request, có throttle và custom domain. Rẻ hơn REST API bốn lần.

  3. 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ũ.

  4. 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í.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.

02 — Bối cảnh

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ốngLấy gìBỏ gì
Grafana OnCallData 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.
GoAlertModel 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.
AlertmanagerRouting 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.
PagerDutyKhá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.
03 — Phạm vi

Làm gì và cố tình không làm gì

Có trong MVP
  • 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
Không làm — và đó là hợp đồng
  • 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.

04 — Requirements

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
IDYêu cầuNghiệm thu
FR-1.1Webhook AlertmanagerTrả 202 trong dưới 500ms p95
FR-1.2Webhook Grafana AlertingCùng contract
FR-1.3Webhook generic, map field bằng JSONPathTest với payload tuỳ ý
FR-1.4Xác thực bằng integration key 32 byte trong pathKey sai trả 401, không lộ thông tin
FR-1.5Không mất alert khi downstream lỗiGhi SQS xong mới trả 202, có DLQ
FR-1.6Gửi lại cùng payload không tạo alert trùngMessageDeduplicationId + conditional write
FR-2Gom nhóm và khử trùng lặp
IDYêu cầuNghiệm thu
FR-2.1Fingerprint = sha256 của integration + label trong group_by10 alert cùng fingerprint chỉ notify một lần
FR-2.2group_by cấu hình được, mặc định alertname, namespace, serviceĐổi trong YAML, apply, có hiệu lực
FR-2.3Alert mới khi group đang mở thì gộp vào, không escalate lạialert_count tăng, không có tin nhắn mới
FR-2.4Alert mới khi group đã đóng thì tạo group mới
FR-2.5Alertmanager gửi resolved thì tự đóng groupTimeline ghi resolved_by system
FR-3Routing và escalation
IDYêu cầuNghiệm thu
FR-3.1Route theo label matcher, first-match-wins, có route mặc địnhRoute mặc định là bắt buộc
FR-3.2Escalation policy nhiều bước, mỗi bước có target và wait_after
FR-3.3Target: người đang trực, một user cụ thể, hoặc một group chat
FR-3.4Chưa ack sau wait_after thì sang bước tiếpKhông ack 5 phút, người thứ hai nhận trong 5m ± 15s
FR-3.5Ack hoặc resolve thì dừng escalation ngay
FR-3.6Bước cuối lặp lại đến khi có người trả lờiChống trường hợp cả team ngủ. Tối đa 50 lần
FR-3.7Escalation sống sót qua deploy và Lambda restartState ở Step Functions và DynamoDB, không giữ trong memory
FR-4Lịch trực
IDYêu cầuNghiệm thu
FR-4.1Sync từ Google Calendar iCal mỗi 5 phútSửa lịch, 5 phút sau CLI trả về đúng người
FR-4.2Map event sang user qua tiêu đề oncall: tên
FR-4.3Hỗ trợ event lặp lại theo RRULETest với rotation hàng tuần
FR-4.4Trả lời "ai đang trực lúc T" dưới 50msMaterialize shift vào DB, không parse ICS lúc runtime
FR-4.5Override thủ công đè lên lịchMột lệnh CLI
FR-4.6Không có ai trực thì fallback và báo adminBắt buộc — lịch trống hoặc sync lỗi là failure mode hay bị quên nhất
FR-4.7Lưu UTC, hiển thị theo Asia/Ho_Chi_Minh
FR-5Thông báo và phản hồi
IDYêu cầuNghiệm thu
FR-5.1Telegram DM với nút Ack, Resolve, Silence 1h, RunbookBấm Ack đổi state dưới 2 giây
FR-5.2User liên kết tài khoản bằng lệnh /link
FR-5.3Telegram lỗi thì retry 3 lần, hết thì escalate ngayKhông chờ hết wait_after
FR-5.4Không gửi trùng khi hệ thống retrydedupKey theo group, step, kênh, user
FR-5.5Sau khi ack thì sửa tin nhắn cũ thành "Acked by @X"
FR-5.6Tin 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.3State machine chặt, transition sai trả 409Idempotent với at-least-once delivery
FR-6.5Mọi thay đổi ghi timeline: ai, làm gì, lúc nào, từ đâu
FR-8SLA và tự giám sát
IDYêu cầuNghiệm thu
FR-8.1Emit metric qua CloudWatch EMF cho mọi sự kiện
FR-8.2Grafana board nội bộ: availability, latency, delivery, MTTA, MTTR
FR-8.3Board 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.4Dead-man switch chạy trên hạ tầng độc lậpBắ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
05 — Kiến trúc

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 FunctionsB · Cron sweeper 1 phútC · EventBridge one-time
Độ chính xác±1s±60s±1s
Trace và debugĐồ thị execution có sẵnPhải đọc logPhải đọc log
RetryBuilt-inTự codeTự code
Code phải viếtÍt nhấtNhiều nhất — cần distributed lockTrung bình
Chi phí / tháng~1,03 USD~0,10 USD~0,05 USD
Chạy ngoài AWSKhôngĐượcKhô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.

06 — Luồng end-to-end

Từ alert đến ack

  1. T+0s
    Alertmanager POST tới /v1/int/{key}/alertmanager
  2. T+0.1s · λ ingest
    Verify key, normalize, đẩy vào SQS FIFO, trả 202. Không chạm database.
  3. T+0.5s · λ processor
    Tí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.
  4. T+1.5s · λ notifier
    Tra 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.
  5. T+2m · người trực bấm Ack
    Telegram 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.
  6. T+6m30s · Step Functions kiểm tra
    State đã là acked → workflow kết thúc, không escalate. Nếu vẫn firing thì sang người thứ hai.
  7. T+25m · Alertmanager gửi resolved
    Group đóng, TTL 90 ngày bắt đầu đếm, emit time_to_resolve_seconds.
07 — Dữ liệu

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.

EntitypkskGhi chú
AlertGroupAG#<ulid>METAGSI1 theo state để list alert đang firing
Alert thôAG#<ulid>ALERT#<ts>TTL 90 ngày
TimelineAG#<ulid>LOG#<ts>Một query lấy hết alert kèm lịch sử
Dedupe pointerFP#<int>#<fp>OPENConditional put chống race hai alert đến cùng lúc
Escalation policyEP#<id>STEP#<order>
ShiftSCHED#<id>SHIFT#<startISO>Query ngược, limit 1 để biết ai đang trực
ContactUSER#<id>CONTACT#telegramGSI1 theo chat_id để tra ngược từ Telegram
SLA rollupSLO#<sli>DAY#<date>
Race condition duy nhất đáng lo

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.

08 — Cam kết

99,9% nghĩa là gì

Error budget = 43 phút 49 giây mỗi tháng

SLIĐịnh nghĩaTarget
Ingest availabilityTỉ lệ request vào endpoint webhook không trả 5xx, cửa sổ 28 ngày99,9%
Notification latencyTỉ lệ alert có tin nhắn đầu tiên gửi đi trong vòng 30 giây99%
Delivery successTỉ lệ tin nhắn được provider xác nhận đã nhận99,5%
Escalation correctnessTỉ lệ bước escalation chạy đúng hạn trong sai số 30 giây99%
Cần thống nhất trước khi ký SLA

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.

09 — Chi phí

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ínhUSD/tháng
Lambda~200k invoke, phần lớn trong free tier0,30
API Gateway HTTP150k request0,15
DynamoDB on-demand~500k đọc, 200k ghi0,50
SQS FIFOTrong free tier0,00
Step Functions45k state transition, 4k đầu miễn phí1,03
EventBridge Scheduler~45k invoke0,05
CloudWatch Logs~2GB ingest1,20
CloudWatch Metrics15 custom metric — khoản lớn nhất, hơi bất ngờ4,50
SSM · Cognito · ACM · TelegramFree tier0,00
Route 53Một hosted zone0,50
GrafanaDùng instance on-prem sẵn có0,00
Tổng8,25
Muốn rẻ hơn nữa

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.

So sánh

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.

10 — Kế hoạch

Bốn giai đoạn

PHASE 0 Nền móng 3–4 ngày

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.

PHASE 1 MVP 2–3 tuần

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.

Bảy điều kiện nghiệm thu
  1. 1Alertmanager thật trên RKE2 fire alert, nhận Telegram dưới 15 giây
  2. 2Mười alert cùng fingerprint chỉ ra một tin nhắn
  3. 3Không ack 5 phút, người thứ hai nhận được
  4. 4Bấm Ack, escalation dừng và tin nhắn được sửa
  5. 5Alertmanager gửi resolved, group tự đóng
  6. 6Sửa Google Calendar, 5 phút sau CLI trả về đúng người
  7. 7terraform destroy rồi apply, hệ thống chạy lại từ đầu
PHASE 2 SLA và vận hành 1–2 tuần

Metric đầy đủ, rollup SLA, hai Grafana board, dead-man switch, silence và maintenance window, Cognito với ba role, CLI hoàn chỉnh.

PHASE 3 iOS — chỉ khi Telegram không đủ 2–3 tuần

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.

PHASE 4 Scale

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.

11 — Rủi ro

Cái gì có thể hỏng

Mức độRủi roCách xử lý
CRITICALBananaOnCall chết mà không ai biếtDead-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.
HIGHScope creepDanh sách không-làm ở mục 03 là hợp đồng. Mọi thứ ngoài đó đẩy sang phase 2 trở đi.
HIGHTelegram bị chặn hoặc API sậpProvider 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.
HIGHAWS region outageChấp nhận ở MVP, ghi rõ trong SLA. Phase 4 làm region thứ hai.
MEDIUMSync Google Calendar lỗiKhô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.
MEDIUMAlert storm nghìn alert một phútSQS làm bộ đệm, Lambda reserved concurrency, grouping cắt phần lớn. Phase 2 thêm rate limit theo integration.
MEDIUMExecution history của Step Functions đầyGiới hạn repeat 50 lần rồi kết thúc và báo admin.
12 — Cần bạn chốt

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.

  1. Q1Step Functions hay cron sweeper? Chọn sweeper nếu bạn ưu tiên tối đa khả năng port sang RKE2.
  2. Q2Hiện có bao nhiêu người trong rotation? Có primary và secondary hay chỉ một tầng?
  3. Q3Board công khai report uptime của BananaOnCall, của các service nó giám sát, hay cả hai?
  4. Q4Domain cho endpoint?
  5. Q5Region ap-southeast-1 Singapore — xác nhận?
  6. Q6Dùng AWS account có sẵn hay tạo account riêng cho project?
  7. 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?
  8. Q8Quy ước Google Calendar: tiêu đề oncall: tên hay dùng email người tham dự?
  9. Q9Public repo ngay hay private trước? Ảnh hưởng tới cách quản lý secret.
  10. Q10Có cần đẩy alert vào Elastic hoặc Kafka sẵn có để lưu trữ dài hạn không?