Files
miti99bot/docs/deploy-aws.md
T
tiennm99 53578d888b fix(deploy): include coin module in ModulesCSV overrides
coin was registered in the catalog and template default but absent from
the ModulesCSV override in both deploy.yml and samconfig.toml, so the
deployed Lambda never loaded it and all coin_* commands went unregistered.

Add a registration checklist to docs/deploy-aws.md to prevent recurrence.
2026-06-12 11:22:16 +07:00

8.4 KiB

Deploy: AWS (Lambda + DynamoDB + EventBridge)

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.

Trading income events API

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

  • TRADING_INCOME_EVENTS_API_URL: FireAnt base URL; defaults to https://restv2.fireant.vn. The bot calls /symbols/{symbol}/timescale-marks with startDate and endDate.
  • TRADING_INCOME_EVENTS_API_TOKEN: bearer token for FireAnt. Store it directly only for local dev; in AWS prefer TRADING_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.

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,trading,stats,gold.

Commands:

  • /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 spot price, total value, invested amount, and P&L.

Price source: v1 uses world spot XAU from GoldPrice.org converted through USD/VND from ExchangeRate-API open endpoint. It is not SJC local retail buy/sell pricing. The defaults require no secrets. Optional overrides:

  • GOLD_PRICE_API_URL: gold spot JSON endpoint override. Remote URLs must be HTTPS; localhost HTTP is allowed for local tests.
  • GOLD_FX_API_URL: 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.