Files
miti99bot/docs/deploy-aws.md
T
tiennm99 15343f6fa7 docs(selfhost): add Coolify deploy + AWS decommission guides; retire AWS docs
Add comprehensive Coolify deployment guide with MongoDB Atlas + networking setup.
Add AWS decommission runbook for safe teardown. Update AWS guides to note
deprecation. Update main README with self-hosted quickstart and architecture overview.
2026-06-28 09:58:37 +07:00

10 KiB

Deploy: AWS (Lambda + DynamoDB + EventBridge)

RETIRED. miti99bot is now self-hosted on Coolify + MongoDB Atlas — see deploy-coolify-selfhosted.md. The AWS stack is decommissioned (aws-decommission-runbook.md). Kept for historical reference / if AWS is ever revisited.

This is the production deploy path for miti99bot. Strict free-tier targets, region ap-southeast-1.

First-time setup: see aws/README.md. This doc is for steady-state operations.

Architecture (one diagram)

Telegram ──HTTPS──► Lambda Function URL (AuthType: NONE)
                    └─► AWS Lambda Web Adapter ──► localhost:8080
                                                    └─► Go http.Handler (cmd/server)
                                                          ├─► DynamoDB (KV)
                                                          ├─► Gemini API (AI modules)
                                                          └─► Telegram Bot API (replies)

EventBridge Scheduler ──cron──► HTTPS POST <FunctionURL>/cron/{name}
                                + Header X-Cron-Token (from SSM)

Deploy

Via GitHub Actions (canonical)

git push origin main

Triggers .github/workflows/deploy.yml:

  1. OIDC assume github-deploy-miti99bot role
  2. make build-lambda (Go ARM64 ZIP-ready binary)
  3. sam deploy --template-file template.yaml
  4. Smoke curl <function-url>/

Manual (emergency / staging)

make build-lambda
make sam-deploy            # uses samconfig.toml defaults
ALERT_EMAIL=you@example.com make sam-deploy   # with budget alert wired

Verify

make logs SINCE=10m

aws cloudformation describe-stacks --stack-name miti99bot \
  --query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text

curl -fsSL "$(...)/" | jq .                       # health JSON

Set the Telegram webhook

.github/workflows/deploy.yml auto-runs setWebhook + setMyCommands after every push to main. The snippet below is the break-glass equivalent for manual / out-of-band fixes (e.g. rerun from a workstation when CI is unavailable).

URL=$(aws cloudformation describe-stacks --stack-name miti99bot \
        --query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text)
SECRET=$(aws ssm get-parameter --name /miti99bot/prod/telegram-webhook-secret \
        --with-decryption --query 'Parameter.Value' --output text)
TOKEN=$(aws ssm get-parameter --name /miti99bot/prod/telegram-bot-token \
        --with-decryption --query 'Parameter.Value' --output text)

curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
  -d "url=${URL}webhook" \
  -d "secret_token=$SECRET" \
  -d "drop_pending_updates=false" \
  -d "allowed_updates=[\"message\",\"callback_query\"]"

Verify:

curl "https://api.telegram.org/bot$TOKEN/getWebhookInfo" | jq .

Expect: url matches Function URL, pending_update_count ≈ 0, last_error_date empty.

Adding a module or command (registration checklist)

A module only runs in production if its name is in both ModulesCSV sources — the template.yaml default is ignored once an override is passed, so editing one place is not enough. A command only appears in the Telegram menu if it is in aws/telegram-commands.json. Missing either is silent: no error, the command just never dispatches (this is how coin_* shipped dark until coin was added to the CSVs).

When adding a new module, register it in all of:

  1. cmd/server/main.go — add the factory to the catalog ("name": pkg.New).
  2. .github/workflows/deploy.yml — append the name to ModulesCSV=… (CI override).
  3. samconfig.toml — append the name to ModulesCSV=… (manual-deploy override; keep in sync with the workflow).
  4. template.yaml — append to the ModulesCSV Default (documents the full set).
  5. aws/telegram-commands.json — add each new command + description for the Telegram menu.

When adding a command to an existing, already-enabled module, only step 5 applies.

On push to main: CI redeploys and re-runs setMyCommands from aws/telegram-commands.json automatically. The Telegram client caches the command menu, so a changed menu may not show until the chat is reopened — confirm with make telegram-commands-info (calls getMyCommands) rather than trusting the app UI. Only when a push introduces new public commands (VisibilityPublic) does the menu need attention — re-confirm registration for those pushes; routine pushes (refactors, fixes, non-public commands) need no menu action.

Stock income events API

/stock_income_events uses a FireAnt REST API, configured at Lambda runtime:

  • STOCK_INCOME_EVENTS_API_URL: FireAnt base URL; defaults to https://restv2.fireant.vn. The bot calls /symbols/{symbol}/timescale-marks with startDate and endDate.
  • STOCK_INCOME_EVENTS_API_TOKEN: bearer token for FireAnt. Store it directly only for local dev; in AWS prefer STOCK_INCOME_EVENTS_API_TOKEN_PARAMETER_NAME.

FireAnt response is an array of timescale marks with id, label, date, title, and color. The bot keeps marks whose label/title indicate dividends, ex-right dates, final registration dates, rights issues, or bonus/share dividends.

Stock price providers

/stock_buy, /stock_sell, and /stock_stats use unofficial public quote endpoints. Zero-value provider order is:

  1. KBS current price board (/stock/iss).
  2. VCI current quote board (/price/symbols/getList).
  3. SSI iBoard direct quote.

This order is intentional for current-price commands. KBS and VCI both support batch current quotes, while SSI can return a Cloudflare security page. Treat all three as unofficial app-internal endpoints and keep provider/source errors visible in Lambda logs.

Gold module

gold is opt-in for first deploy. Enable it by adding gold to the ModulesCSV parameter / MODULES env, for example util,misc,wordle,loldle,lolschedule,twentyq,stock,stats,gold.

Commands:

  • /gold_price shows current gold price. When VNAppMob SJC is available it prints buy/sell/mid VND/lượng; otherwise it falls back to world spot XAU in USD/oz plus USD/VND rate.
  • /gold_topup <amount> credits VND. No currency argument is accepted.
  • /gold_buy <luong> buys gold in Vietnamese luong. No symbol or unit argument is accepted.
  • /gold_sell <luong> sells gold in Vietnamese luong.
  • /gold_stats shows VND balance, gold holding, current price, total value, invested amount, and P&L.

Price source: primary is VNAppMob Vietnam SJC price feed (api.vnappmob.com/api/v2/gold/sjc), which returns VND/lượng directly. The client auto-refreshes a free JWT API key and caches it in KV. If VNAppMob fails, the bot falls back to world spot XAU from GoldPrice.org converted through USD/VND from ExchangeRate-API open endpoint. The defaults require no secrets. Optional overrides:

  • GOLD_VNAPP_API_URL: VNAppMob API base URL override. Remote URLs must be HTTPS; localhost HTTP is allowed for local tests.
  • GOLD_VNAPP_API_KEY: pre-issued VNAppMob JWT key. When set, auto-refresh is skipped. Useful for local dev or SSM injection.
  • GOLD_VNAPP_API_KEY_PARAMETER_NAME: SSM SecureString parameter name containing the pre-issued key. Fetched at Lambda cold start.
  • GOLD_PRICE_API_URL: fallback gold spot JSON endpoint override. Remote URLs must be HTTPS; localhost HTTP is allowed for local tests.
  • GOLD_FX_API_URL: fallback USD/VND FX JSON endpoint override. Remote URLs must be HTTPS; localhost HTTP is allowed for local tests.

ExchangeRate-API open endpoint requires attribution if surfaced publicly and updates once per day; the bot caches FX responses until the provider time_next_update_unix when available.

Rotate secrets

aws ssm put-parameter --name /miti99bot/prod/telegram-webhook-secret \
  --value "$(openssl rand -hex 32)" --type SecureString --overwrite
# template.yaml uses ":1" version pin; redeploy to pick up the new value:
make sam-deploy
# Then re-run setWebhook (above) with the new secret_token.

The :1 in {{resolve:ssm-secure:…:1}} is the parameter version — it pins to the latest version at deploy time, not version 1 forever. To force a refresh after rotation, redeploy.

Rollback

CloudFormation handles failed deploys: a failing sam deploy triggers automatic rollback to the prior version. To roll back a successful-but-bad deploy:

aws cloudformation update-stack \
  --stack-name miti99bot \
  --use-previous-template \
  --capabilities CAPABILITY_IAM

Or redeploy from a known-good commit:

git checkout <good-sha>
make sam-deploy

Operational checks (daily during 7-day soak)

# Errors / warnings in last 24h
aws logs filter-log-events --log-group-name /aws/lambda/miti99bot \
  --start-time $(($(date +%s%3N) - 86400000)) \
  --filter-pattern '{ $.level = "ERROR" }' --max-items 20

# Cold start P95
aws logs start-query --log-group-name /aws/lambda/miti99bot \
  --start-time $(($(date +%s) - 86400)) --end-time $(date +%s) \
  --query-string 'filter @type = "REPORT" | stats avg(@initDuration), pct(@initDuration, 95)'

# DynamoDB throttle
aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB \
  --metric-name ThrottledRequests --dimensions Name=TableName,Value=miti99bot-data \
  --statistics Sum --start-time $(date -u -d '24 hours ago' +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) --period 3600

# Current month spend
aws ce get-cost-and-usage --granularity MONTHLY \
  --time-period Start=$(date -u +%Y-%m-01),End=$(date -u +%F) \
  --metrics UnblendedCost

Free-tier guardrails

Resource Free Watch when
Lambda req / GB-s 1M / 400k Past 50% mid-month
DynamoDB req 200M Past 5% (sign of runaway loop)
DynamoDB storage 25 GiB Past 100 MiB (suspect leaks)
EventBridge invocations 14M Past 1k/mo (suspect mis-config)
CloudWatch Logs ingest 5 GB Past 50% mid-month
Egress 100 GB Past 1 GB (wildly high)

A $1 budget alert at 80%/100% catches all of these via cost-side fallout.