mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
refactor: rename module to miti99bot, canonicalize AWS deploy path
Rename: - Go module github.com/tiennm99/miti99bot-go → github.com/tiennm99/miti99bot - CloudFormation stack miti99bot-aws-port → miti99bot - Drop "port", "Cloud Run", "GCP", "cutover", "Phase NN" framing from active code and docs — project reads as canonical AWS-Lambda from now on. AWS deploy guide + flow fix: - New docs/deploy-aws-free-tier-guide.md — Ubuntu 24.04 ARM64 onboarding with project-local venv (pip awscli + sam-cli), SSM secrets via read -s, idempotent OIDC provider + role creation, $1 budget alarm. - Drop sam build from the pipeline — provided.al2023 + makefile builder expects a Makefile in CodeUri (build/lambda/, the output dir), so the step always fails. sam deploy --template-file template.yaml now reads the raw template and zips build/lambda/ directly. - Rollback section rewritten — use continue-update-rollback / cancel-update-stack / git-SHA redeploy. Drop the broken --use-previous-template recipe. - DynamoDB free-tier row corrected (on-demand is 2.5M read / 1M write request units, not 25 RCU/WCU). Updated: - README.md fully rewritten (drops port/legacy framing, lists modules, points new users at the free-tier guide). - aws/README.md retitled "AWS account setup", phase numbers stripped. - Makefile / .github/workflows/deploy.yml — sam deploy flow. - samconfig.toml — stack_name = "miti99bot". - Go comments — Cloud Run → Lambda, Cloud Scheduler → EventBridge Scheduler, Cloud Logging → CloudWatch Logs. - Struct field GCPProject → FirestoreProject (env GOOGLE_CLOUD_PROJECT unchanged). Plus advisory reports under plans/reports/ from the code-reviewer + researcher passes that informed the fixes. Verified: go vet ./..., go build ./..., go test ./... all green.
This commit is contained in:
1 parent
e29d7afa0e
commit
642fccb7b7
73 files changed
+1340
-251
No files matched your search
@@ -63,7 +63,7 @@ jobs:
|
||||
run: go build ./...
|
||||
|
||||
- name: docker build
|
||||
run: docker build -t miti99bot-go .
|
||||
run: docker build -t miti99bot .
|
||||
|
||||
iac:
|
||||
name: SAM template validate
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AWS_REGION: ap-southeast-1
|
||||
STACK_NAME: miti99bot-aws-port
|
||||
STACK_NAME: miti99bot
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -40,20 +40,17 @@ jobs:
|
||||
- name: Build Lambda binary
|
||||
run: make build-lambda
|
||||
|
||||
- name: SAM build
|
||||
run: sam build
|
||||
|
||||
- name: SAM deploy
|
||||
env:
|
||||
ALERT_EMAIL: ${{ secrets.ALERT_EMAIL }}
|
||||
run: |
|
||||
if [ -n "$ALERT_EMAIL" ]; then
|
||||
sam deploy \
|
||||
sam deploy --template-file template.yaml \
|
||||
--no-confirm-changeset \
|
||||
--no-fail-on-empty-changeset \
|
||||
--parameter-overrides "AlertEmail=$ALERT_EMAIL"
|
||||
else
|
||||
sam deploy \
|
||||
sam deploy --template-file template.yaml \
|
||||
--no-confirm-changeset \
|
||||
--no-fail-on-empty-changeset
|
||||
fi
|
||||
|
||||
@@ -24,7 +24,7 @@ firestore-emulator: ## Start Firestore emulator on :8085 (foreground)
|
||||
# to already be running (use `make firestore-emulator` in another shell).
|
||||
test-emulator: ## Run tests with Firestore emulator (must be running)
|
||||
FIRESTORE_EMULATOR_HOST=localhost:8085 \
|
||||
GOOGLE_CLOUD_PROJECT=miti99bot-go-test \
|
||||
GOOGLE_CLOUD_PROJECT=miti99bot-test \
|
||||
go test -race -count=1 ./...
|
||||
|
||||
# Run DynamoDB integration tests against DynamoDB Local.
|
||||
@@ -78,19 +78,21 @@ dynamodb-local-stop: ## Stop DynamoDB Local
|
||||
sam-validate: ## Validate template.yaml without contacting AWS
|
||||
sam validate --lint
|
||||
|
||||
sam-build: build-lambda ## sam build (after make build-lambda)
|
||||
sam build
|
||||
sam-build: build-lambda ## Produce the Lambda artifact (alias for build-lambda; sam deploy uses raw template directly)
|
||||
@echo "Artifact ready at build/lambda/bootstrap; sam deploy --template-file template.yaml will zip it."
|
||||
|
||||
sam-deploy: sam-build ## Deploy via SAM (uses samconfig.toml). Set ALERT_EMAIL=… optionally.
|
||||
sam-deploy: build-lambda ## Deploy via SAM (uses samconfig.toml). Set ALERT_EMAIL=… optionally.
|
||||
@if [ -n "$$ALERT_EMAIL" ]; then \
|
||||
sam deploy --no-confirm-changeset --no-fail-on-empty-changeset \
|
||||
sam deploy --template-file template.yaml \
|
||||
--no-confirm-changeset --no-fail-on-empty-changeset \
|
||||
--parameter-overrides "AlertEmail=$$ALERT_EMAIL"; \
|
||||
else \
|
||||
sam deploy --no-confirm-changeset --no-fail-on-empty-changeset; \
|
||||
sam deploy --template-file template.yaml \
|
||||
--no-confirm-changeset --no-fail-on-empty-changeset; \
|
||||
fi
|
||||
|
||||
logs: ## Tail Lambda logs (last 5m). Override with SINCE=10m.
|
||||
@sam logs --tail --stack-name miti99bot-aws-port --start-time $${SINCE:-5m}ago
|
||||
@sam logs --tail --stack-name miti99bot --start-time $${SINCE:-5m}ago
|
||||
|
||||
# ---- Clean ----------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
# miti99bot-go
|
||||
# miti99bot
|
||||
|
||||
Plug-n-play Telegram bot framework in Go. **Default deploy: AWS Lambda + DynamoDB + EventBridge Scheduler (free tier).** Cloud Run + Firestore retained as an alt path. Free-tier port of [miti99bot](https://github.com/tiennm99/miti99bot).
|
||||
Plug-n-play Telegram bot framework in Go. Runs on AWS Lambda + DynamoDB + EventBridge Scheduler. Strictly free-tier.
|
||||
|
||||
## Status
|
||||
## Modules
|
||||
|
||||
Mid-port. Code is on `main`; first AWS deploy still pending the user's manual AWS-account bootstrap.
|
||||
| Module | What it does |
|
||||
|---|---|
|
||||
| `util` | `/help`, `/info`, `/stickerid` |
|
||||
| `misc` | Coin flip, dice, RNG utilities |
|
||||
| `wordle` | Daily Wordle game |
|
||||
| `loldle` | League-of-Legends "guess the champion" |
|
||||
| `lolschedule` | Pro-match schedule + daily push |
|
||||
| `twentyq` | 20-questions game (requires Gemini API key) |
|
||||
| `trading` | VN-stocks paper trading |
|
||||
|
||||
| Track | What | Status |
|
||||
|-------|------|--------|
|
||||
| Modules | util, misc, wordle, loldle, lolschedule, twentyq, trading | **done** |
|
||||
| Storage | KVStore interface; in-memory + Firestore + **DynamoDB** providers | **done** |
|
||||
| AI | Gemini API client (`internal/ai`) | **done** |
|
||||
| AWS IaC | SAM template + Makefile + GH Actions OIDC deploy | **done** |
|
||||
| AWS bootstrap | account, IAM OIDC, SSM SecureString params, first `sam deploy` | **manual user steps — see [`aws/README.md`](aws/README.md)** |
|
||||
| Cron handlers | lolschedule daily push, trading daily refresh | pending |
|
||||
| Trading module | VN-stocks paper trading | pending |
|
||||
| Cutover | Telegram webhook flip + 7-day soak | deploy-gated |
|
||||
|
||||
Plans:
|
||||
- Active: [`plans/260510-0234-pre-deploy-wrapup/`](plans/260510-0234-pre-deploy-wrapup/plan.md) — cron handlers + trading + cosmetics
|
||||
- AWS port: [`plans/260510-0114-aws-port/`](plans/260510-0114-aws-port/plan.md)
|
||||
- Original GCP plan (historical): [`plans/260508-2222-go-port-cloud-run/`](plans/260508-2222-go-port-cloud-run/plan.md) — module work reused; deploy phases superseded by AWS port
|
||||
Disable any module by editing `MODULES` in `template.yaml`.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -29,16 +23,17 @@ cmd/server/ entrypoint
|
||||
internal/server/ HTTP routes (/, /webhook, /cron/{name})
|
||||
internal/telegram/ Telegram webhook + bot wrapper
|
||||
internal/modules/ Module framework, registry, dispatchers, modules
|
||||
internal/storage/ KVStore interface; memory / firestore / dynamodb providers
|
||||
internal/ai/ Gemini client
|
||||
internal/storage/ KVStore interface; memory + dynamodb providers
|
||||
internal/ai/ Gemini client (used by twentyq)
|
||||
template.yaml AWS SAM IaC (Lambda + Function URL + DynamoDB + Logs + Budget)
|
||||
docs/deploy-aws.md AWS deploy operations
|
||||
aws/README.md One-time AWS account bootstrap cheatsheet
|
||||
docs/deploy-aws-free-tier-guide.md Full onboarding guide
|
||||
docs/deploy-aws.md Steady-state operations
|
||||
aws/README.md One-time AWS account setup
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
In-memory KV (default; no AWS / no GCP needed):
|
||||
In-memory KV (no AWS required):
|
||||
|
||||
```sh
|
||||
TELEGRAM_BOT_TOKEN=… \
|
||||
@@ -56,30 +51,21 @@ make dynamodb-local # docker run amazon/dynamodb-local on :8001
|
||||
make test-dynamodb # runs internal/storage tests against DDB Local
|
||||
```
|
||||
|
||||
For Firestore emulator (legacy):
|
||||
```sh
|
||||
make firestore-emulator # in a second shell
|
||||
make test-emulator
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```sh
|
||||
make vet # go vet
|
||||
make test # full unit suite (no emulator)
|
||||
make test-dynamodb # storage tests against DynamoDB Local (requires Docker)
|
||||
make test-emulator # storage tests against Firestore emulator
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
**AWS (canonical):** see [`docs/deploy-aws.md`](docs/deploy-aws.md). Push to `main` → GitHub Actions OIDC → SAM deploy. First-time bootstrap: [`aws/README.md`](aws/README.md).
|
||||
First-time onboarding: see [`docs/deploy-aws-free-tier-guide.md`](docs/deploy-aws-free-tier-guide.md).
|
||||
|
||||
**Cloud Run (alternative, deferred):** the multi-stage `Dockerfile` builds an image suitable for any container runtime (Cloud Run, Fly.io, ECS Fargate, K8s). Image is `golang:1.25-alpine` → `gcr.io/distroless/static:nonroot`, ~15 MiB.
|
||||
Steady-state operations: [`docs/deploy-aws.md`](docs/deploy-aws.md).
|
||||
|
||||
```sh
|
||||
docker build -t miti99bot-go .
|
||||
```
|
||||
After the initial setup, every push to `main` triggers `.github/workflows/deploy.yml` (GitHub Actions OIDC → SAM deploy). No long-lived AWS keys.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+25
-24
@@ -1,21 +1,23 @@
|
||||
# AWS bootstrap commands
|
||||
# AWS account setup
|
||||
|
||||
One-time setup steps for Phase 01 of the AWS port. After this is done, every push to `main` deploys via GitHub Actions OIDC; no human-in-loop AWS commands needed.
|
||||
One-time setup steps for a fresh AWS account. After this is done, every push to `main` deploys via GitHub Actions OIDC; no human-in-loop AWS commands needed.
|
||||
|
||||
> For the full onboarding walkthrough (prerequisites, Telegram wiring, cost guardrails), see [`../docs/deploy-aws-free-tier-guide.md`](../docs/deploy-aws-free-tier-guide.md). This file is the condensed cheatsheet.
|
||||
|
||||
> **Region:** `ap-southeast-1` (Singapore). Change in `samconfig.toml` if needed.
|
||||
> **Stack name:** `miti99bot-aws-port`. Change in `samconfig.toml`.
|
||||
> **Stack name:** `miti99bot`. Change in `samconfig.toml`.
|
||||
|
||||
---
|
||||
|
||||
## 1. AWS account hygiene
|
||||
|
||||
1. Enable MFA on the root user.
|
||||
2. Create an IAM admin user `bootstrap-admin` (CLI access keys). Use only for the first `sam deploy --guided`.
|
||||
2. Create an IAM admin user `admin` (CLI access keys). Use only for the first `sam deploy --guided`.
|
||||
3. Set CLI default region:
|
||||
```sh
|
||||
aws configure set region ap-southeast-1 --profile bootstrap-admin
|
||||
aws configure set aws_access_key_id AKIA… --profile bootstrap-admin
|
||||
aws configure set aws_secret_access_key … --profile bootstrap-admin
|
||||
aws configure set region ap-southeast-1 --profile admin
|
||||
aws configure set aws_access_key_id AKIA… --profile admin
|
||||
aws configure set aws_secret_access_key … --profile admin
|
||||
```
|
||||
|
||||
## 2. SSM Parameter Store secrets
|
||||
@@ -24,16 +26,16 @@ Create the four required secrets. **Names must match `template.yaml`** (`/miti99
|
||||
|
||||
```sh
|
||||
aws ssm put-parameter --name /miti99bot/prod/telegram-bot-token \
|
||||
--value "<bot-father-token>" --type SecureString --profile bootstrap-admin
|
||||
--value "<bot-father-token>" --type SecureString --profile admin
|
||||
|
||||
aws ssm put-parameter --name /miti99bot/prod/telegram-webhook-secret \
|
||||
--value "$(openssl rand -hex 32)" --type SecureString --profile bootstrap-admin
|
||||
--value "$(openssl rand -hex 32)" --type SecureString --profile admin
|
||||
|
||||
aws ssm put-parameter --name /miti99bot/prod/gemini-api-key \
|
||||
--value "<google-ai-studio-key>" --type SecureString --profile bootstrap-admin
|
||||
--value "<google-ai-studio-key>" --type SecureString --profile admin
|
||||
|
||||
aws ssm put-parameter --name /miti99bot/prod/cron-shared-secret \
|
||||
--value "$(openssl rand -hex 32)" --type SecureString --profile bootstrap-admin
|
||||
--value "$(openssl rand -hex 32)" --type SecureString --profile admin
|
||||
```
|
||||
|
||||
Save the webhook + cron secrets locally — you'll set them on the Telegram side and on the EventBridge schedule headers.
|
||||
@@ -47,7 +49,7 @@ aws iam create-open-id-connect-provider \
|
||||
--url https://token.actions.githubusercontent.com \
|
||||
--client-id-list sts.amazonaws.com \
|
||||
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 \
|
||||
--profile bootstrap-admin
|
||||
--profile admin
|
||||
```
|
||||
|
||||
(GitHub publishes the canonical thumbprint; verify on docs.github.com if rotated.)
|
||||
@@ -60,9 +62,9 @@ Edit `aws/iam-github-oidc-trust.json` to set your AWS account ID and GitHub repo
|
||||
aws iam create-role \
|
||||
--role-name github-deploy-miti99bot \
|
||||
--assume-role-policy-document file://aws/iam-github-oidc-trust.json \
|
||||
--profile bootstrap-admin
|
||||
--profile admin
|
||||
|
||||
# Permissions (broad to start; tighten in Phase 06).
|
||||
# Permissions (broad to start; tighten with stack-scoped policies later).
|
||||
for arn in \
|
||||
arn:aws:iam::aws:policy/AWSCloudFormationFullAccess \
|
||||
arn:aws:iam::aws:policy/AWSLambda_FullAccess \
|
||||
@@ -75,11 +77,11 @@ for arn in \
|
||||
arn:aws:iam::aws:policy/IAMFullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonS3FullAccess; do
|
||||
aws iam attach-role-policy --role-name github-deploy-miti99bot \
|
||||
--policy-arn "$arn" --profile bootstrap-admin
|
||||
--policy-arn "$arn" --profile admin
|
||||
done
|
||||
```
|
||||
|
||||
> Yes, this is broad. SAM creates IAM roles for the Lambda, so the deploy role needs `iam:CreateRole`. **Tighten in Phase 06** with custom policies scoped to the stack's resource ARNs.
|
||||
> Yes, this is broad. SAM creates IAM roles for the Lambda, so the deploy role needs `iam:CreateRole`. **Tighten later** with custom policies scoped to the stack's resource ARNs.
|
||||
|
||||
## 5. Add GitHub repo secrets
|
||||
|
||||
@@ -92,31 +94,30 @@ In GitHub repo settings → Secrets and variables → Actions:
|
||||
|
||||
`AWS_ACCOUNT_ID` is not a credential — it's hidden only to keep the ARN out of the workflow file.
|
||||
|
||||
## 6. First deploy (manual, with bootstrap admin)
|
||||
## 6. First deploy (manual)
|
||||
|
||||
```sh
|
||||
make build-lambda
|
||||
sam build
|
||||
AWS_PROFILE=bootstrap-admin sam deploy --guided
|
||||
AWS_PROFILE=admin sam deploy --template-file template.yaml --guided
|
||||
```
|
||||
|
||||
Confirm:
|
||||
- Stack name: `miti99bot-aws-port`
|
||||
- Stack name: `miti99bot`
|
||||
- Region: `ap-southeast-1`
|
||||
- Capabilities: `CAPABILITY_IAM`
|
||||
- Save to `samconfig.toml`: yes (already committed; this just confirms)
|
||||
|
||||
After `CREATE_COMPLETE`:
|
||||
```sh
|
||||
aws cloudformation describe-stacks --stack-name miti99bot-aws-port \
|
||||
--query "Stacks[0].Outputs" --output table --profile bootstrap-admin
|
||||
aws cloudformation describe-stacks --stack-name miti99bot \
|
||||
--query "Stacks[0].Outputs" --output table --profile admin
|
||||
```
|
||||
Note the `FunctionUrl` — Phase 07 sets the Telegram webhook to it.
|
||||
Note the `FunctionUrl` — point the Telegram webhook at it (see [`../docs/deploy-aws-free-tier-guide.md`](../docs/deploy-aws-free-tier-guide.md) Step 5).
|
||||
|
||||
## 7. Tighten — optional but recommended
|
||||
|
||||
Once the first deploy succeeds:
|
||||
1. Rotate / delete `bootstrap-admin` CLI keys (use only via console for emergencies).
|
||||
1. Rotate / delete `admin` CLI keys (use only via console for emergencies).
|
||||
2. Trigger a workflow_dispatch deploy via GH Actions to confirm OIDC path works without the bootstrap user.
|
||||
3. Replace the broad managed policies on `github-deploy-miti99bot` with stack-scoped custom policies.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Federated": "arn:aws:iam::REPLACE_WITH_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
|
||||
"Federated": "arn:aws:iam::225603493174:oidc-provider/token.actions.githubusercontent.com"
|
||||
},
|
||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
||||
"Condition": {
|
||||
@@ -13,9 +13,9 @@
|
||||
},
|
||||
"StringLike": {
|
||||
"token.actions.githubusercontent.com:sub": [
|
||||
"repo:tiennm99/miti99bot-go:ref:refs/heads/main",
|
||||
"repo:tiennm99/miti99bot-go:ref:refs/heads/dev",
|
||||
"repo:tiennm99/miti99bot-go:pull_request"
|
||||
"repo:tiennm99/miti99bot:ref:refs/heads/main",
|
||||
"repo:tiennm99/miti99bot:ref:refs/heads/dev",
|
||||
"repo:tiennm99/miti99bot:pull_request"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+19
-19
@@ -12,20 +12,20 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/metrics"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/loldle"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/lolschedule"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/misc"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/trading"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/twentyq"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/wordle"
|
||||
"github.com/tiennm99/miti99bot-go/internal/server"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/telegram"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/metrics"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/loldle"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/lolschedule"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/misc"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/trading"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/twentyq"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/wordle"
|
||||
"github.com/tiennm99/miti99bot/internal/server"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/telegram"
|
||||
)
|
||||
|
||||
// factories is the static module catalog. Adding a new module is a one-line
|
||||
@@ -45,7 +45,7 @@ func factories() map[string]modules.Factory {
|
||||
|
||||
// firestoreInitTimeout caps Firestore client construction at startup. Cloud
|
||||
// Run cold start budget is 500ms target; firestore.NewClient is normally fast
|
||||
// but network blips can make it hang. Fail fast and let Cloud Run restart us.
|
||||
// but network blips can make it hang. Fail fast and let Lambda restart us.
|
||||
const firestoreInitTimeout = 10 * time.Second
|
||||
|
||||
// dynamodbInitTimeout caps DynamoDB client construction at startup. Lambda
|
||||
@@ -160,7 +160,7 @@ func buildProvider(ctx context.Context, cfg config) (storage.KVProvider, func(),
|
||||
switch {
|
||||
case os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "":
|
||||
backend = "dynamodb"
|
||||
case cfg.GCPProject != "" || cfg.FirestoreEmulatorHost != "":
|
||||
case cfg.FirestoreProject != "" || cfg.FirestoreEmulatorHost != "":
|
||||
backend = "firestore"
|
||||
default:
|
||||
backend = "memory"
|
||||
@@ -175,7 +175,7 @@ func buildProvider(ctx context.Context, cfg config) (storage.KVProvider, func(),
|
||||
case "firestore":
|
||||
// Emulator ignores the project ID but the SDK still requires *some*
|
||||
// non-empty value; supply a placeholder so emulator-only local dev works.
|
||||
projectID := cfg.GCPProject
|
||||
projectID := cfg.FirestoreProject
|
||||
if projectID == "" && cfg.FirestoreEmulatorHost != "" {
|
||||
projectID = "miti99bot-emulator"
|
||||
}
|
||||
@@ -223,7 +223,7 @@ type config struct {
|
||||
TelegramBotToken string
|
||||
WebhookSecret string
|
||||
CronSecret string
|
||||
GCPProject string
|
||||
FirestoreProject string
|
||||
FirestoreEmulatorHost string
|
||||
GeminiAPIKey string
|
||||
Modules []string
|
||||
@@ -255,7 +255,7 @@ func loadConfig() config {
|
||||
TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"],
|
||||
WebhookSecret: envMap["TELEGRAM_WEBHOOK_SECRET"],
|
||||
CronSecret: envMap["CRON_SHARED_SECRET"],
|
||||
GCPProject: envMap["GOOGLE_CLOUD_PROJECT"],
|
||||
FirestoreProject: envMap["GOOGLE_CLOUD_PROJECT"],
|
||||
FirestoreEmulatorHost: envMap["FIRESTORE_EMULATOR_HOST"],
|
||||
GeminiAPIKey: envMap["GEMINI_API_KEY"],
|
||||
Modules: splitCSV(envMap["MODULES"]),
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
# Deploy miti99bot to AWS (Free Tier)
|
||||
|
||||
End-to-end onboarding guide for deploying `miti99bot` on AWS. Everything below stays inside the AWS free tier in region `ap-southeast-1` (Singapore).
|
||||
|
||||
Related docs:
|
||||
- One-time bootstrap reference: [`aws/README.md`](../aws/README.md)
|
||||
- Steady-state operations: [`deploy-aws.md`](./deploy-aws.md)
|
||||
|
||||
---
|
||||
|
||||
## What you get (all free-tier)
|
||||
|
||||
| Resource | Free quota | This bot's usage |
|
||||
|---|---|---|
|
||||
| Lambda (ARM64, 256 MB) | 1M req + 400k GB-s / mo, **always-free** | far below |
|
||||
| Lambda Function URL | included with Lambda | the Telegram webhook entrypoint |
|
||||
| DynamoDB on-demand | 25 GiB storage + 2.5M read / 1M write request units / mo **always-free** | far below |
|
||||
| EventBridge Scheduler | 14M invocations / mo always-free | a few crons |
|
||||
| SSM Parameter Store (Standard) | unlimited free | 4 SecureString params |
|
||||
| CloudWatch Logs | 5 GB ingest, 5 GB storage / mo | well below at 7-day retention |
|
||||
| SQS (cron DLQ) | 1M req / mo always-free | near zero |
|
||||
| CloudFormation, IAM, Budgets | free | |
|
||||
| Egress | 100 GB / mo always-free | tiny |
|
||||
|
||||
Paid traps the template already avoids: DynamoDB PITR disabled, no NAT Gateway, no API Gateway, no provisioned concurrency, no VPC, log retention pinned to 7 days, X-Ray "Active" tracing stays within the 100k traces/mo free tier.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites (Ubuntu 24.04 ARM64)
|
||||
|
||||
Host arch matches Lambda's `arm64` target, so `make build-lambda` is a native build (still pinned to `GOARCH=arm64` for reproducibility).
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y curl jq make git python3 python3-venv python3-pip
|
||||
```
|
||||
|
||||
### AWS CLI + SAM CLI (project-local venv via pip)
|
||||
|
||||
Ubuntu 24.04 enforces PEP 668 (externally-managed system Python), so we install both tools inside a project-local `.venv`. From the repo root:
|
||||
|
||||
```sh
|
||||
cd /path/to/miti99bot
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install awscli aws-sam-cli
|
||||
|
||||
aws --version # aws-cli/1.x (pip ships v1; v2 is not on PyPI)
|
||||
sam --version
|
||||
```
|
||||
|
||||
Activate the venv at the start of every shell session you use for AWS commands:
|
||||
|
||||
```sh
|
||||
source /path/to/miti99bot/.venv/bin/activate
|
||||
```
|
||||
|
||||
If you want auto-activation, install [`direnv`](https://direnv.net/) (`sudo apt install direnv`, then `eval "$(direnv hook bash)"` in `~/.bashrc`) and drop a `.envrc` in the repo containing `source .venv/bin/activate`. Don't override the shell built-in `cd` — that affects every directory you ever enter.
|
||||
|
||||
> **Note:** PyPI's `awscli` is **v1** (v2 is only distributed as the standalone bundle). v1 covers every command used in this guide — `ssm`, `iam`, `cloudformation`, `lambda`, `logs`, `ce`, `cloudwatch`. If you later need v2-only features (e.g. SSO login, new `aws configure sso` flows), install v2 separately from the official ARM zip and keep both. SAM CLI on PyPI tracks upstream releases — `pip install -U aws-sam-cli` to bump.
|
||||
|
||||
Add `.venv/` to `.gitignore` if not already there:
|
||||
|
||||
```sh
|
||||
grep -qxF '.venv/' .gitignore || echo '.venv/' >> .gitignore
|
||||
```
|
||||
|
||||
### Go 1.25 (ARM64)
|
||||
|
||||
Ubuntu 24.04 ships an older Go. Install the upstream tarball:
|
||||
|
||||
```sh
|
||||
GO_VERSION=1.25.0
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz" -o /tmp/go.tgz
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf /tmp/go.tgz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
go version # expect go1.25.x linux/arm64
|
||||
```
|
||||
|
||||
### GitHub CLI (`gh`)
|
||||
|
||||
```sh
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
| sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install -y gh
|
||||
```
|
||||
|
||||
### Docker (optional — only for DynamoDB Local tests)
|
||||
|
||||
```sh
|
||||
sudo apt install -y docker.io
|
||||
sudo usermod -aG docker "$USER"
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
### Accounts / keys
|
||||
|
||||
- AWS account (root access).
|
||||
- Telegram bot token from BotFather.
|
||||
- Gemini API key from Google AI Studio (free).
|
||||
- This repo cloned locally.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — AWS account hygiene
|
||||
|
||||
1. Log in to the AWS root user, enable MFA.
|
||||
2. Create an IAM user `admin` with `AdministratorAccess` and CLI access keys. This is used only for the first deploy.
|
||||
3. Configure the CLI:
|
||||
```sh
|
||||
aws configure set region ap-southeast-1 --profile admin
|
||||
aws configure set aws_access_key_id AKIA… --profile admin
|
||||
aws configure set aws_secret_access_key … --profile admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Store the 4 secrets in SSM Parameter Store
|
||||
|
||||
Parameter Store Standard tier is free; SecureString uses the AWS-managed KMS key, also free. `--tier Standard` is the default — never pass `--tier Advanced` (that costs $0.05/param/month).
|
||||
|
||||
> **Shell-history hygiene.** Long-lived tokens (BotFather, Gemini) are passed below via `read -s` so they never appear in `~/.bash_history`, `ps`, or backups of either. The two short-lived random tokens (webhook + cron) are generated inline with `openssl rand`.
|
||||
|
||||
Real secrets — read each value interactively, no echo:
|
||||
|
||||
```sh
|
||||
read -rsp 'BotFather token: ' BOT_TOKEN && echo
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-bot-token --value "$BOT_TOKEN"
|
||||
unset BOT_TOKEN
|
||||
|
||||
read -rsp 'Gemini API key: ' GEMINI_KEY && echo
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/gemini-api-key --value "$GEMINI_KEY"
|
||||
unset GEMINI_KEY
|
||||
```
|
||||
|
||||
> Skip the Gemini one if you're not using the `twentyq` module — store `"unused"` so CloudFormation can still resolve it, then drop `twentyq` from `MODULES`.
|
||||
|
||||
Generated secrets — random hex, no input needed:
|
||||
|
||||
```sh
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-webhook-secret --value "$(openssl rand -hex 32)"
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/cron-shared-secret --value "$(openssl rand -hex 32)"
|
||||
```
|
||||
|
||||
The webhook + cron values are fetched back in Step 5 via `aws ssm get-parameter` — no need to copy them out by hand.
|
||||
|
||||
Confirm all four exist (names only, no values):
|
||||
|
||||
```sh
|
||||
aws ssm get-parameters-by-path --profile admin \
|
||||
--path /miti99bot/prod/ --query 'Parameters[].Name' --output table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Register GitHub OIDC + deploy role (one-time)
|
||||
|
||||
> Run all commands in this step from the repo root — `aws/iam-github-oidc-trust.json` is referenced as a relative path.
|
||||
|
||||
```sh
|
||||
cd /path/to/miti99bot
|
||||
```
|
||||
|
||||
Register the OIDC identity provider (idempotent — skips creation if it already exists):
|
||||
|
||||
```sh
|
||||
ACCT=$(aws sts get-caller-identity --profile admin --query Account --output text)
|
||||
OIDC_ARN="arn:aws:iam::${ACCT}:oidc-provider/token.actions.githubusercontent.com"
|
||||
|
||||
aws iam get-open-id-connect-provider --profile admin \
|
||||
--open-id-connect-provider-arn "$OIDC_ARN" >/dev/null 2>&1 \
|
||||
|| aws iam create-open-id-connect-provider --profile admin \
|
||||
--url https://token.actions.githubusercontent.com \
|
||||
--client-id-list sts.amazonaws.com \
|
||||
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
|
||||
```
|
||||
|
||||
Edit `aws/iam-github-oidc-trust.json` — fill in your 12-digit AWS account ID and `tiennm99/miti99bot` (or your fork):
|
||||
|
||||
```sh
|
||||
sed -i "s|225603493174|$ACCT|" aws/iam-github-oidc-trust.json # only if the placeholder differs
|
||||
```
|
||||
|
||||
Create the role (idempotent — `update-assume-role-policy` if it already exists):
|
||||
|
||||
```sh
|
||||
aws iam get-role --profile admin --role-name github-deploy-miti99bot >/dev/null 2>&1 \
|
||||
&& aws iam update-assume-role-policy --profile admin \
|
||||
--role-name github-deploy-miti99bot \
|
||||
--policy-document file://aws/iam-github-oidc-trust.json \
|
||||
|| aws iam create-role --profile admin \
|
||||
--role-name github-deploy-miti99bot \
|
||||
--assume-role-policy-document file://aws/iam-github-oidc-trust.json
|
||||
```
|
||||
|
||||
Attach the managed policies (re-attaching the same policy is a no-op, so this loop is safely re-runnable):
|
||||
|
||||
```sh
|
||||
for arn in \
|
||||
arn:aws:iam::aws:policy/AWSCloudFormationFullAccess \
|
||||
arn:aws:iam::aws:policy/AWSLambda_FullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonSQSFullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonSSMFullAccess \
|
||||
arn:aws:iam::aws:policy/CloudWatchLogsFullAccess \
|
||||
arn:aws:iam::aws:policy/AWSBudgetsActionsWithAWSResourceControlAccess \
|
||||
arn:aws:iam::aws:policy/IAMFullAccess \
|
||||
arn:aws:iam::aws:policy/AmazonS3FullAccess; do
|
||||
aws iam attach-role-policy --profile admin \
|
||||
--role-name github-deploy-miti99bot --policy-arn "$arn"
|
||||
done
|
||||
```
|
||||
|
||||
These managed policies are intentionally broad for the first deploy. `IAMFullAccess` and `AmazonS3FullAccess` are the widest blast radius — tighten them first when you reach Step 7.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — First deploy (manual)
|
||||
|
||||
```sh
|
||||
cd /path/to/miti99bot
|
||||
make build-lambda # cross-compiles Go → linux/arm64
|
||||
AWS_PROFILE=admin sam deploy --template-file template.yaml --guided # accept samconfig.toml defaults
|
||||
```
|
||||
|
||||
> **Why `--template-file template.yaml`.** By default `sam deploy` looks for `.aws-sam/build/template.yaml` (output of `sam build`). We skip `sam build` because SAM's default builder for `provided.al2023` expects a `Makefile` inside `CodeUri: build/lambda/` — which is the *output* directory of `make build-lambda`, not a source dir. Pointing `sam deploy` at the raw source template tells it to read `CodeUri: build/lambda/` directly, zip the bootstrap binary, upload it, and deploy. The `make build-lambda` step above is the actual compile.
|
||||
|
||||
Confirm at the SAM prompt:
|
||||
- Stack name: `miti99bot`
|
||||
- Region: `ap-southeast-1`
|
||||
- Capabilities: `CAPABILITY_IAM`
|
||||
- Save to `samconfig.toml`: yes
|
||||
|
||||
After `CREATE_COMPLETE`, grab the Function URL:
|
||||
|
||||
```sh
|
||||
aws cloudformation describe-stacks --profile admin \
|
||||
--stack-name miti99bot \
|
||||
--query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Point Telegram at the webhook
|
||||
|
||||
```sh
|
||||
URL=… # from previous command
|
||||
TOKEN=$(aws ssm get-parameter --profile admin \
|
||||
--name /miti99bot/prod/telegram-bot-token --with-decryption \
|
||||
--query Parameter.Value --output text)
|
||||
SECRET=$(aws ssm get-parameter --profile admin \
|
||||
--name /miti99bot/prod/telegram-webhook-secret --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 "allowed_updates=[\"message\",\"callback_query\"]"
|
||||
|
||||
curl "https://api.telegram.org/bot$TOKEN/getWebhookInfo" | jq .
|
||||
```
|
||||
|
||||
Expect: `url` matches Function URL, `pending_update_count` ≈ 0, `last_error_date` empty. Send `/start` to the bot — response should arrive within a couple seconds.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Wire GitHub Actions for future deploys
|
||||
|
||||
In GitHub → repo Settings → Secrets and variables → Actions:
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `AWS_ACCOUNT_ID` | 12-digit AWS account ID |
|
||||
| `ALERT_EMAIL` (optional) | Email for the $1 budget alert |
|
||||
|
||||
After this, every push to `main` triggers `.github/workflows/deploy.yml`:
|
||||
1. OIDC assume `github-deploy-miti99bot` role
|
||||
2. `make build-lambda`
|
||||
3. `sam deploy --template-file template.yaml`
|
||||
4. Smoke `curl <function-url>/`
|
||||
|
||||
No long-lived keys live in GitHub.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Lock down (recommended once it works)
|
||||
|
||||
1. Rotate / delete `admin` CLI keys (keep the user for console-only emergencies).
|
||||
2. Trigger a `workflow_dispatch` deploy via GH Actions to confirm OIDC path works without the bootstrap user.
|
||||
3. Replace the broad managed policies on `github-deploy-miti99bot` with stack-scoped custom policies (resource ARNs from your stack).
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Cost guardrails
|
||||
|
||||
- Set `ALERT_EMAIL` → enables a $1/mo AWS Budgets alarm at 80% and 100%.
|
||||
- Daily checks (logs, DDB throttle, cold-start P95, MTD spend) → see [`deploy-aws.md`](./deploy-aws.md) "Operational checks".
|
||||
- Idle steady-state cost is **$0**. If you ever see >$0.01 in Cost Explorer, investigate. Most likely culprits:
|
||||
- CloudWatch Logs ingestion volume (verbose logging, hot loops).
|
||||
- DynamoDB writes from a runaway loop.
|
||||
- Accidental egress past the 100 GB free tier.
|
||||
|
||||
---
|
||||
|
||||
## Free-tier watch table
|
||||
|
||||
| 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) |
|
||||
|
||||
The $1 budget alarm catches all of these via cost-side fallout.
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
CloudFormation has three rollback flavors. Pick the one that matches your situation.
|
||||
|
||||
### Case A — `sam deploy` is currently failing
|
||||
|
||||
CloudFormation auto-initiates a rollback. Nothing to do. If the rollback itself fails (`UPDATE_ROLLBACK_FAILED`):
|
||||
|
||||
```sh
|
||||
aws cloudformation continue-update-rollback --profile admin \
|
||||
--stack-name miti99bot
|
||||
```
|
||||
|
||||
### Case B — `sam deploy` is running and you want to abort
|
||||
|
||||
```sh
|
||||
aws cloudformation cancel-update-stack --profile admin \
|
||||
--stack-name miti99bot
|
||||
```
|
||||
|
||||
CloudFormation rolls back to the prior `CREATE_COMPLETE` / `UPDATE_COMPLETE` state.
|
||||
|
||||
### Case C — Deploy succeeded but the code is bad
|
||||
|
||||
CloudFormation has no "redeploy previous template" command (`--use-previous-template` re-applies the *current* template, not an older one — it does **not** roll back). Redeploy from the last known-good commit:
|
||||
|
||||
```sh
|
||||
git checkout <good-sha>
|
||||
make build-lambda
|
||||
make sam-deploy
|
||||
```
|
||||
|
||||
To find the last good SHA quickly: `git log --oneline -- template.yaml cmd/server` and pick the commit that matches a passing deploy.
|
||||
|
||||
---
|
||||
|
||||
## Rotating secrets
|
||||
|
||||
```sh
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-webhook-secret \
|
||||
--value "$(openssl rand -hex 32)"
|
||||
make sam-deploy # picks up the latest SSM value
|
||||
# Then re-run setWebhook (Step 5) with the new secret_token.
|
||||
```
|
||||
|
||||
> **The `:1` in `template.yaml` is not "version 1 forever".** `{{resolve:ssm-secure:…:1}}` is a CloudFormation pin to *whatever version 1 means at deploy time* — the literal integer is a required syntax element, not a frozen index. After `put-parameter --overwrite`, SSM bumps the version number; CloudFormation reads the latest at the next `sam deploy` and updates the Lambda env. If you want zero-redeploy rotation, switch `main.go` to fetch from SSM at startup or per-request instead of consuming the env var.
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
# Deploy: AWS (Lambda + DynamoDB + EventBridge)
|
||||
|
||||
This is the production deploy path for `miti99bot-go`. Strict free-tier targets, region `ap-southeast-1`.
|
||||
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.
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 build && sam deploy`
|
||||
3. `sam deploy --template-file template.yaml`
|
||||
4. Smoke `curl <function-url>/`
|
||||
|
||||
### Manual (emergency / staging)
|
||||
@@ -45,16 +45,16 @@ ALERT_EMAIL=you@example.com make sam-deploy # with budget alert wired
|
||||
```sh
|
||||
make logs SINCE=10m
|
||||
|
||||
aws cloudformation describe-stacks --stack-name miti99bot-aws-port \
|
||||
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 (Phase 07)
|
||||
## Set the Telegram webhook
|
||||
|
||||
```sh
|
||||
URL=$(aws cloudformation describe-stacks --stack-name miti99bot-aws-port \
|
||||
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)
|
||||
@@ -92,7 +92,7 @@ CloudFormation handles failed deploys: a failing `sam deploy` triggers automatic
|
||||
|
||||
```sh
|
||||
aws cloudformation update-stack \
|
||||
--stack-name miti99bot-aws-port \
|
||||
--stack-name miti99bot \
|
||||
--use-previous-template \
|
||||
--capabilities CAPABILITY_IAM
|
||||
```
|
||||
@@ -107,18 +107,18 @@ make sam-deploy
|
||||
|
||||
```sh
|
||||
# Errors / warnings in last 24h
|
||||
aws logs filter-log-events --log-group-name /aws/lambda/miti99bot-aws-port-bot \
|
||||
aws logs filter-log-events --log-group-name /aws/lambda/miti99bot-bot \
|
||||
--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-aws-port-bot \
|
||||
aws logs start-query --log-group-name /aws/lambda/miti99bot-bot \
|
||||
--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-aws-port-data \
|
||||
--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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/tiennm99/miti99bot-go
|
||||
module github.com/tiennm99/miti99bot
|
||||
|
||||
go 1.25.0
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ var ErrRateLimited = errors.New("ai: rate limited")
|
||||
var ErrNotConfigured = errors.New("ai: GEMINI_API_KEY not set")
|
||||
|
||||
// Client wraps a *genai.Client with the small surface the bot needs. The
|
||||
// underlying gRPC connection is reused across requests — Cloud Run cold-start
|
||||
// underlying gRPC connection is reused across requests — Lambda cold-start
|
||||
// budget makes a per-request handshake intolerable.
|
||||
//
|
||||
// Safe for concurrent use; *genai.Client is itself goroutine-safe.
|
||||
@@ -35,7 +35,7 @@ type Client struct {
|
||||
|
||||
// NewClient constructs a *Client backed by the Gemini API (not Vertex AI —
|
||||
// Vertex requires a service-account flow incompatible with the free-tier
|
||||
// Cloud Run baseline). A blank apiKey returns ErrNotConfigured so callers
|
||||
// Lambda baseline). A blank apiKey returns ErrNotConfigured so callers
|
||||
// can decide whether to skip AI-dependent module loading.
|
||||
func NewClient(ctx context.Context, apiKey string) (*Client, error) {
|
||||
if strings.TrimSpace(apiKey) == "" {
|
||||
|
||||
@@ -15,8 +15,7 @@ import (
|
||||
//
|
||||
// Why we don't enforce daily caps here: x/time/rate is a token bucket, not
|
||||
// a fixed-window counter. Per-day caps need a different abstraction; if we
|
||||
// hit RPD limits in practice we'll add a Firestore-backed counter. Phase 11
|
||||
// soak data will tell us if it's needed.
|
||||
// hit RPD limits in practice we'll add a DynamoDB-backed counter.
|
||||
type PerUserLimiter struct {
|
||||
mu sync.Mutex
|
||||
buckets map[string]*rate.Limiter
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//
|
||||
// Trade-off: the underlying sync.Map grows unboundedly with distinct keys
|
||||
// (~32 B each). At 1M keys that's ~32 MB — acceptable for the lifetime of
|
||||
// a Cloud Run instance, which restarts well before reaching that scale.
|
||||
// Eviction is a Phase 11 concern, not a v1 one.
|
||||
// a Lambda instance, which restarts well before reaching that scale.
|
||||
// Eviction is intentionally deferred — restart frequency keeps the working set bounded.
|
||||
package keylock
|
||||
|
||||
import "sync"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// Package log is a thin facade over stdlib log/slog with a JSON handler
|
||||
// preconfigured for Cloud Logging. Cloud Run reads stdout line-by-line; with
|
||||
// a JSON line, Cloud Logging picks up `severity`, `message`, and `time` and
|
||||
// preconfigured for CloudWatch Logs. Lambda reads stdout line-by-line; with
|
||||
// a JSON line, CloudWatch Logs picks up `severity`, `message`, and `time` and
|
||||
// surfaces remaining fields as structured labels for filtering.
|
||||
//
|
||||
// Why a facade instead of importing slog directly: (1) callers stay
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// Package metrics is a tiny in-memory counter store with periodic flush
|
||||
// to Cloud Logging via the project's structured logger.
|
||||
// to CloudWatch Logs via the project's structured logger.
|
||||
//
|
||||
// Why not Prometheus / OpenTelemetry: the project runs on Cloud Run free
|
||||
// Why not Prometheus / OpenTelemetry: the project runs on Lambda free
|
||||
// tier with scale-to-zero. A pull-based exporter would be scraped from
|
||||
// outside the instance and routinely hit a cold pod, defeating the point.
|
||||
// Push-based exporters (StatsD, OTLP) require a paid sink.
|
||||
//
|
||||
// Cloud Logging is already free up to a generous quota and supports
|
||||
// CloudWatch Logs is already free up to a generous quota and supports
|
||||
// log-based metrics (count over `jsonPayload.msg=metrics`) for dashboards
|
||||
// and alerts. Per-instance counters are reset on flush so the log line
|
||||
// represents a delta, which Cloud Logging's count aggregation can sum
|
||||
// represents a delta, which CloudWatch Logs's count aggregation can sum
|
||||
// across instances and time windows.
|
||||
package metrics
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
)
|
||||
|
||||
// DefaultFlushInterval is how often Run flushes counters to the log. 60s
|
||||
@@ -32,7 +32,7 @@ const DefaultFlushInterval = 60 * time.Second
|
||||
//
|
||||
// Counters use atomic.Int64 so increments don't lock; the per-name map
|
||||
// itself is guarded by an RWMutex for the rare add path. Names should be
|
||||
// short and stable — they become Cloud Logging label values.
|
||||
// short and stable — they become CloudWatch Logs label values.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
commands map[string]*atomic.Int64
|
||||
@@ -131,7 +131,7 @@ func drain(m map[string]*atomic.Int64) map[string]int64 {
|
||||
//
|
||||
// {"msg":"metrics","commands":{"wordle":3,"loldle":1},"errors":{"ai-429":1},"ai":null}
|
||||
//
|
||||
// Cloud Logging filters on `jsonPayload.msg=metrics` for dashboards.
|
||||
// CloudWatch Logs filters on `jsonPayload.msg=metrics` for dashboards.
|
||||
// Empty categories appear as null (slog's default for nil maps).
|
||||
func (r *Registry) Flush() {
|
||||
cmds, errs, ai := r.snapshot()
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
logger "github.com/tiennm99/miti99bot-go/internal/log"
|
||||
logger "github.com/tiennm99/miti99bot/internal/log"
|
||||
)
|
||||
|
||||
// captureLogger swaps the package-level logger for one writing to buf and
|
||||
|
||||
@@ -11,7 +11,7 @@ var ErrCronNotFound = errors.New("cron not found")
|
||||
|
||||
// DispatchScheduled runs the cron registered under name with the per-module
|
||||
// prefixed Deps the registry stored at Build time. Returns ErrCronNotFound if
|
||||
// no module owns that name — Cloud Scheduler hitting an unknown route is a
|
||||
// no module owns that name — EventBridge Scheduler hitting an unknown route is a
|
||||
// configuration bug worth surfacing as a 404 at the HTTP layer.
|
||||
//
|
||||
// The handler runs synchronously in the calling goroutine; ctx propagates
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/metrics"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/metrics"
|
||||
)
|
||||
|
||||
// Auth gates Protected/Private commands by sender Telegram user ID. Public
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
const newRoundHint = "🆕 Send <code>/loldle</code> or <code>/loldle <champion></code> to start a new round."
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// installLoldle wires the loldle module + auth (owner gates /loldle_setmax,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package loldle
|
||||
|
||||
import (
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the loldle module Factory. Loads champions.json once at construction
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// MaxGuesses is the default round length. Per-subject overrides land via
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func TestGameState_StartedAtNullByDefault(t *testing.T) {
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,14 +32,14 @@ const (
|
||||
// Riot's own public JS bundle and serves the live site too.
|
||||
// #nosec G101
|
||||
apiKey = "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z"
|
||||
userAgent = "miti99bot-go/0.1 (https://t.me/miti99bot)"
|
||||
userAgent = "miti99bot/0.1 (https://t.me/miti99bot)"
|
||||
// CacheTTL: schedule data changes minute-by-minute during live events.
|
||||
cacheTTL = 120 * time.Second
|
||||
// staleMaxAge: how long to fall back to a cached payload when the
|
||||
// upstream call fails outright.
|
||||
staleMaxAge = 60 * 60 * time.Second
|
||||
// httpTimeout: keep upstream calls bounded so a hung lolesports edge
|
||||
// can't hold a Cloud Run instance.
|
||||
// can't hold a Lambda instance.
|
||||
httpTimeout = 8 * time.Second
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// mkServer spins an httptest.Server returning the supplied JSON body for
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// dailyPushCronName is the cron route segment + EventBridge schedule key.
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
const dailyPushCronName = "lolschedule_daily_push"
|
||||
|
||||
// dailyPushSchedule is documentation only — the real fire time lives in
|
||||
// AWS EventBridge Scheduler / Cloud Scheduler. Cron expression is UTC;
|
||||
// EventBridge Scheduler. Cron expression is UTC;
|
||||
// 01:00 UTC == 08:00 ICT.
|
||||
const dailyPushSchedule = "0 1 * * *"
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// fakeSender records every SendMessage call. errOn returns an error for the
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// state captures everything a lolschedule handler needs at runtime.
|
||||
@@ -86,7 +86,7 @@ func (s *state) replyForRange(ctx context.Context, b *bot.Bot, chatID int64, fro
|
||||
}
|
||||
|
||||
// handleSubscribe is /lolschedule_subscribe — opt the chat into the daily
|
||||
// digest (push wiring lands with Phase 09 Cloud Scheduler).
|
||||
// digest delivered by the EventBridge Scheduler cron handler.
|
||||
func (s *state) handleSubscribe(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
||||
msg := update.Message
|
||||
if msg == nil {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// installSchedule wires the lolschedule module to a recording bot, with a
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package lolschedule
|
||||
|
||||
import (
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the lolschedule module Factory. The 5 user-facing commands plus the
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// subscribersKey is the KV slot holding the per-module subscriber list.
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func TestSubscribers_AddRemoveListIdempotent(t *testing.T) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// installMisc wires the misc module to a recording bot with a fresh
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// lastPingKey is the per-module KV key /ping writes and /mstats reads.
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// We test the per-command KV behaviour directly — the bot/Telegram side is
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// Visibility classifies who may invoke a command. The dispatcher enforces
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
// purely for logging/metrics, not flow control.
|
||||
type CommandHandler func(ctx context.Context, b *bot.Bot, update *models.Update) error
|
||||
|
||||
// CronHandler runs when Cloud Scheduler hits /cron/{name}. Crons receive the
|
||||
// CronHandler runs when EventBridge Scheduler hits /cron/{name}. Crons receive the
|
||||
// per-module-prefixed Deps via the registry; handlers should not capture the
|
||||
// base Deps from the factory closure or KV writes will collide across modules.
|
||||
type CronHandler func(ctx context.Context, deps Deps) error
|
||||
@@ -43,7 +43,7 @@ type Command struct {
|
||||
|
||||
// Cron is a single scheduled job exposed by a module.
|
||||
type Cron struct {
|
||||
Schedule string // documentation only; real schedule lives in Cloud Scheduler
|
||||
Schedule string // documentation only; real schedule lives in EventBridge Scheduler
|
||||
Name string // unique within module
|
||||
Handler CronHandler // required
|
||||
}
|
||||
@@ -74,8 +74,7 @@ type Deps struct {
|
||||
Bot *bot.Bot // nil-safe: only crons that fan-out (lolschedule daily push) need it
|
||||
}
|
||||
|
||||
// Factory constructs a Module from its Deps. Spec deviation: Phase 03 plan
|
||||
// defines `Factory func() Module` with a separate Init step. We pass Deps
|
||||
// directly so handler closures can capture them — idiomatic Go and removes a
|
||||
// lifecycle ordering trap.
|
||||
// Factory constructs a Module from its Deps. Deps are passed directly (instead
|
||||
// of a separate Init step) so handler closures can capture them — idiomatic Go
|
||||
// and removes a lifecycle ordering trap.
|
||||
type Factory func(deps Deps) Module
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// moduleNameRe is intentionally looser than commandNameRe — it allows hyphen
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func noopCmd(name string) Command {
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// state is the per-module runtime. KV is module-scoped (the framework
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// Portfolio is the per-user trading state. Currency is a map for forward-
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func TestLoadPortfolio_FirstTimeUser(t *testing.T) {
|
||||
|
||||
@@ -81,7 +81,7 @@ func (c *PriceClient) FetchPrice(ctx context.Context, ticker string) (float64, e
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("trading: build KBS request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (miti99bot-go)")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (miti99bot)")
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// tickerRe restricts tickers to ASCII alphanumeric, 1-16 chars. Stops
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func TestResolveSymbol_FirstTime_QueriesAndCaches(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package trading
|
||||
|
||||
import (
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the trading module Factory. Five user-facing commands; no crons.
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// scriptedChatter returns canned responses by call index. Tests script the
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// Turn is one Q&A entry. JS-parity field names.
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/binary"
|
||||
mrand "math/rand/v2"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/ai"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/ai"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the twentyq module Factory. If Deps.Chatter is nil
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// installUtil builds a registry with the util module + auth that admits the
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
const repoURL = "https://github.com/tiennm99/miti99bot-go"
|
||||
const repoURL = "https://github.com/tiennm99/miti99bot"
|
||||
|
||||
var supportFooter = fmt.Sprintf(
|
||||
`Enjoying the bot? Support me by starring the repo: <a href="%s">%s</a>`,
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// helpTestNoop is a stand-in handler used only to satisfy the registry's
|
||||
@@ -58,7 +58,7 @@ func TestRenderHelp_GroupsByModuleAndSkipsPrivate(t *testing.T) {
|
||||
// Locks html.EscapeString contract: & → &, " → ".
|
||||
"Tom & "Jerry"",
|
||||
// Support footer always present.
|
||||
"github.com/tiennm99/miti99bot-go",
|
||||
"github.com/tiennm99/miti99bot",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing %q\n---output---\n%s", want, out)
|
||||
@@ -124,7 +124,7 @@ func TestRenderHelp_NilRegistryReturnsFooterOnly(t *testing.T) {
|
||||
if !strings.Contains(out, "no commands registered") {
|
||||
t.Errorf("nil registry should render placeholder; got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "github.com/tiennm99/miti99bot-go") {
|
||||
if !strings.Contains(out, "github.com/tiennm99/miti99bot") {
|
||||
t.Errorf("footer missing; got:\n%s", out)
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
)
|
||||
|
||||
// infoCommand returns /info — replies plain text with chat / thread / sender
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
)
|
||||
|
||||
const stickerIDUsage = "Reply to a sticker message with /stickerid to get its file_id.\n" +
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the module Factory. Closes over Deps so each handler has access to
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/keylock"
|
||||
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// state captures everything a wordle command needs at handler-time. Built
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot-go/internal/testutil"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
// installWordle wires the wordle module to a recording bot with an
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// MaxGuesses is the standard wordle round length.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func TestStats_DefaultLastResultAtIsNull(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
// New is the wordle module Factory. Loads the embedded dictionary once,
|
||||
|
||||
@@ -2,7 +2,7 @@ package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
// HealthHandler answers GET / with a stable string so Cloud Run's HTTP probe
|
||||
// HealthHandler answers GET / with a stable string so Lambda's HTTP probe
|
||||
// and any uptime monitor can distinguish "process up" from "process listening
|
||||
// but routing broken".
|
||||
func HealthHandler() http.HandlerFunc {
|
||||
@@ -17,6 +17,6 @@ func HealthHandler() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("miti99bot-go ok\n"))
|
||||
_, _ = w.Write([]byte("miti99bot ok\n"))
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
)
|
||||
|
||||
// statusRecorder wraps http.ResponseWriter to capture the final status
|
||||
@@ -34,9 +34,8 @@ func (r *statusRecorder) effectiveStatus() int {
|
||||
//
|
||||
// {"msg":"req","method":"POST","path":"/webhook","status":200,"ms":12}
|
||||
//
|
||||
// Cloud Logging filters on `jsonPayload.msg=req AND jsonPayload.status>=500`
|
||||
// for 5xx-rate alerting. Mirrors the JS source's index.js shape so existing
|
||||
// dashboards keep working post-cutover.
|
||||
// CloudWatch Logs filters on `jsonPayload.msg=req AND jsonPayload.status>=500`
|
||||
// for 5xx-rate alerting. Mirrors the JS source's index.js shape.
|
||||
func LogRequests(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
logger "github.com/tiennm99/miti99bot-go/internal/log"
|
||||
logger "github.com/tiennm99/miti99bot/internal/log"
|
||||
)
|
||||
|
||||
// captureLogger swaps the package-level logger for one writing to buf and
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/telegram"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/telegram"
|
||||
)
|
||||
|
||||
// cronNameRe limits cron path segments to a safe alphabet so log injection via
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
// router boundary). Same shape as Telegram command names.
|
||||
var cronNameRe = regexp.MustCompile(`^[a-z0-9_]{1,32}$`)
|
||||
|
||||
// cronAuthHeader is the shared-secret header name. Replaced by OIDC in Phase 09.
|
||||
// cronAuthHeader is the shared-secret header EventBridge Scheduler attaches when
|
||||
// invoking /cron/{name}.
|
||||
const cronAuthHeader = "X-Cron-Token"
|
||||
|
||||
// Config wires the router's runtime dependencies.
|
||||
@@ -29,9 +30,9 @@ type Config struct {
|
||||
Registry *modules.Registry
|
||||
WebhookSecret string
|
||||
|
||||
// CronSecret is the shared-secret bridge until Phase 09 adds OIDC. Empty
|
||||
// means /cron/{name} is fully disabled (404). Required to prevent
|
||||
// unauthenticated triggering of billable side effects.
|
||||
// CronSecret protects /cron/{name} against unauthenticated calls; EventBridge
|
||||
// Scheduler attaches it as the X-Cron-Token header. Empty means /cron/{name}
|
||||
// is fully disabled (404).
|
||||
CronSecret string
|
||||
}
|
||||
|
||||
@@ -39,10 +40,10 @@ type Config struct {
|
||||
//
|
||||
// GET / → health
|
||||
// POST /webhook → Telegram update intake (constant-time secret check)
|
||||
// POST /cron/{name} → Cloud Scheduler entry (shared-secret check; OIDC in Phase 09)
|
||||
// POST /cron/{name} → EventBridge Scheduler entry (shared-secret check)
|
||||
//
|
||||
// Anything else is 404. All routes pass through LogRequests so every
|
||||
// request emits a structured `req` log line (Cloud Logging consumes them
|
||||
// request emits a structured `req` log line (CloudWatch Logs consumes them
|
||||
// for 5xx-rate alerts and per-route latency).
|
||||
func New(cfg Config) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/storage"
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
const testCronSecret = "shared-cron-secret"
|
||||
|
||||
@@ -2,7 +2,7 @@ package server
|
||||
|
||||
import "time"
|
||||
|
||||
// defaultCronTimeout caps a single /cron/{name} invocation. Cloud Run free
|
||||
// defaultCronTimeout caps a single /cron/{name} invocation. Lambda free
|
||||
// tier runs at most 1 instance, so a long cron serializes all other crons
|
||||
// behind it and amplifies any DoS via the cron route. 60s is the budget; long
|
||||
// crons must publish to PubSub and exit fast.
|
||||
|
||||
@@ -24,7 +24,7 @@ func requireEmulator(t *testing.T) *firestore.Client {
|
||||
}
|
||||
project := os.Getenv("GOOGLE_CLOUD_PROJECT")
|
||||
if project == "" {
|
||||
project = "miti99bot-go-test"
|
||||
project = "miti99bot-test"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// MemoryKVStore is an in-process KVStore for tests and local smoke runs.
|
||||
// It is the only implementation available until Phase 04 adds Firestore.
|
||||
// Data is lost on restart; production uses the DynamoDB provider.
|
||||
type MemoryKVStore struct {
|
||||
mu sync.RWMutex
|
||||
m map[string][]byte
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/log"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
)
|
||||
|
||||
// secretTokenHeader is the case-insensitive HTTP header Telegram sets when it
|
||||
@@ -27,7 +27,7 @@ const maxWebhookBody = 1 << 20
|
||||
|
||||
// handlerTimeout caps a single Telegram update handler. Telegram retries after
|
||||
// 60s of no 2xx; 10s leaves headroom for outbound API calls inside handlers
|
||||
// without holding a Cloud Run instance long enough to block other updates.
|
||||
// without holding a Lambda instance long enough to block other updates.
|
||||
const handlerTimeout = 10 * time.Second
|
||||
|
||||
// WebhookHandler returns an http.HandlerFunc that validates Telegram's secret
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
# Deploy AWS Free-Tier Guide — Shell Safety & Correctness Review
|
||||
|
||||
Doc reviewed: `docs/deploy-aws-free-tier-guide.md` (310 lines).
|
||||
Cross-refs: `template.yaml`, `samconfig.toml`, `Makefile`, `aws/iam-github-oidc-trust.json`, `.github/workflows/deploy.yml`, `aws/README.md`.
|
||||
|
||||
## Summary
|
||||
|
||||
Doc is mostly sound but has several copy-paste hazards a new user will hit. Biggest issues:
|
||||
|
||||
1. **Rollback step is wrong.** `update-stack --use-previous-template` re-applies the CURRENT template, not the prior one. Won't roll back a bad-but-successful deploy.
|
||||
2. **No idempotency on bootstrap.** `create-open-id-connect-provider`, `create-role`, and `attach-role-policy` loop have no "already exists" guard. A half-failed Step 3 leaves the user re-running and seeing confusing `EntityAlreadyExists` errors.
|
||||
3. **The `cd()` override is fragile.** Wraps `builtin cd` globally and silently activates ANY repo's `.venv`. Will surprise users who work in multiple projects and breaks shell tools that rely on `cd`'s normal behavior.
|
||||
4. **Privilege blast radius.** Bootstrap `admin` is full root-equivalent (AdministratorAccess) and CI role gets 10 `*FullAccess` policies — including `IAMFullAccess` and `AmazonS3FullAccess`. Doc says "tighten later" but offers no starter narrowing.
|
||||
5. **Webhook URL building is brittle.** `-d "url=${URL}webhook"` assumes a trailing slash on `$URL`. Function URLs end with `/`, so it works — but if a user pastes one without the slash it silently posts to a wrong path.
|
||||
6. **`<placeholder>` values in `aws ssm put-parameter` will be stored literally** if the user blindly copy-pastes.
|
||||
|
||||
Free-tier alignment looks correct vs `template.yaml` (PITR off, retention 7d, ARM64, PAY_PER_REQUEST, Function URL not API GW). X-Ray "Active" tracing is enabled in Globals; doc correctly notes the 100k traces/mo free quota but doesn't tell the user how to monitor it.
|
||||
|
||||
---
|
||||
|
||||
## Critical (must-fix before sharing)
|
||||
|
||||
### C1. Rollback command does not roll back
|
||||
|
||||
Lines 286–292:
|
||||
|
||||
```sh
|
||||
aws cloudformation update-stack \
|
||||
--stack-name miti99bot-aws-port \
|
||||
--use-previous-template --capabilities CAPABILITY_IAM
|
||||
```
|
||||
|
||||
**Failure mode:** `--use-previous-template` means "use the template currently associated with the stack". After a successful but bad deploy, the stack's current template IS the bad one. This command does nothing useful (no-op changeset) — it does NOT revert to the prior version. There is no CFN flag to roll back to a prior template; the user must redeploy from a known-good source.
|
||||
|
||||
**Suggested rewrite:**
|
||||
```sh
|
||||
# CloudFormation does not store prior templates. To revert a bad deploy,
|
||||
# redeploy from a known-good commit:
|
||||
git checkout <good-sha>
|
||||
AWS_PROFILE=admin make sam-deploy
|
||||
# (Auto-rollback only triggers on CREATE/UPDATE failures, not on a deploy
|
||||
# that succeeded but ships a bug.)
|
||||
```
|
||||
|
||||
Drop the `update-stack --use-previous-template` block entirely, or repurpose it as a "continue-rollback" recipe for a stuck `UPDATE_ROLLBACK_FAILED` state (different command: `aws cloudformation continue-update-rollback`).
|
||||
|
||||
---
|
||||
|
||||
### C2. Step 3 OIDC provider + role creation is not re-runnable
|
||||
|
||||
Lines 149–177. If `create-open-id-connect-provider` succeeds but `create-role` fails (typo in JSON, wrong path), the user fixes the JSON and re-runs the block. Both `create-open-id-connect-provider` and `create-role` will then fail with `EntityAlreadyExists` and the user has to manually delete or skip steps.
|
||||
|
||||
**Failure mode:** New users will think bootstrap is broken.
|
||||
|
||||
**Suggested rewrite (guard each):**
|
||||
```sh
|
||||
# OIDC provider (idempotent)
|
||||
ACCOUNT_ID=$(aws sts get-caller-identity --profile admin --query Account --output text)
|
||||
OIDC_ARN="arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
|
||||
if ! aws iam get-open-id-connect-provider --profile admin \
|
||||
--open-id-connect-provider-arn "$OIDC_ARN" >/dev/null 2>&1; then
|
||||
aws iam create-open-id-connect-provider --profile admin \
|
||||
--url https://token.actions.githubusercontent.com \
|
||||
--client-id-list sts.amazonaws.com \
|
||||
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
|
||||
fi
|
||||
|
||||
# Role (idempotent)
|
||||
if ! aws iam get-role --profile admin --role-name github-deploy-miti99bot >/dev/null 2>&1; then
|
||||
aws iam create-role --profile admin \
|
||||
--role-name github-deploy-miti99bot \
|
||||
--assume-role-policy-document file://aws/iam-github-oidc-trust.json
|
||||
else
|
||||
aws iam update-assume-role-policy --profile admin \
|
||||
--role-name github-deploy-miti99bot \
|
||||
--policy-document file://aws/iam-github-oidc-trust.json
|
||||
fi
|
||||
```
|
||||
|
||||
The `attach-role-policy` loop is already idempotent in AWS (attaching twice succeeds), so it's fine — but worth noting in a comment.
|
||||
|
||||
---
|
||||
|
||||
### C3. Working-directory assumption: relative file paths
|
||||
|
||||
Line 161: `--assume-role-policy-document file://aws/iam-github-oidc-trust.json`
|
||||
|
||||
**Failure mode:** AWS CLI resolves `file://aws/...` relative to the **current shell working directory**, not to the repo. Step 4 (line 186) tells the user to `cd /path/to/miti99bot`, but Step 3 has no equivalent `cd`. If the user followed Step 1 from `~`, this fails with `Unable to parse parameter ... no such file`.
|
||||
|
||||
Same risk applies to anyone who opens a new terminal between Step 2 and Step 3.
|
||||
|
||||
**Suggested rewrite:** add the `cd` line at the top of Step 3, OR use an absolute path:
|
||||
```sh
|
||||
# At top of Step 3:
|
||||
cd /path/to/miti99bot # all file:// paths below are relative to repo root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### C4. The `cd() { builtin cd …; }` override is dangerous
|
||||
|
||||
Lines 56–62:
|
||||
|
||||
```sh
|
||||
miti99bot_venv() {
|
||||
[ -f "$PWD/.venv/bin/activate" ] && source "$PWD/.venv/bin/activate"
|
||||
}
|
||||
cd() { builtin cd "$@" && miti99bot_venv; }
|
||||
```
|
||||
|
||||
**Failure modes:**
|
||||
1. **Globally scoped to ALL repos** — auto-activates any `.venv/` the user happens to `cd` into, including unrelated Python projects. Quietly clobbers PYTHONPATH/PATH expectations.
|
||||
2. **No deactivation** when leaving the repo. The venv stays active forever in that shell.
|
||||
3. **Inherits no `cd` options** — `cd -P`, `cd -L`, `cd -e`, etc. still work via `"$@"`, but tools that check `type cd` or `command -v cd` (some shell scripts and `pyenv`-style hooks) see a function and may misbehave.
|
||||
4. **Breaks subshells / scripts that `source` a tool's init** if they `unset -f cd` or override it again.
|
||||
5. **Persistent in `~/.bashrc`** — survives long after the user finishes onboarding.
|
||||
|
||||
**Suggested rewrite:** drop the `cd` override entirely. Recommend `direnv` (already mentioned but only as a comment) with a `.envrc` file checked into the repo, or just tell users to `source .venv/bin/activate` once per shell.
|
||||
|
||||
```sh
|
||||
# Option A (recommended): install direnv, add repo .envrc with `source .venv/bin/activate`
|
||||
# Option B: just activate manually
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
If keeping the auto-activate hint, scope it strictly to this repo path:
|
||||
```sh
|
||||
miti99bot_venv() {
|
||||
case "$PWD" in
|
||||
*/miti99bot|*/miti99bot/*) [ -f "$HOME/path/to/miti99bot/.venv/bin/activate" ] && \
|
||||
source "$HOME/path/to/miti99bot/.venv/bin/activate" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### C5. Placeholders in `aws ssm put-parameter` will be stored literally
|
||||
|
||||
Lines 134, 138 (and Step 8/rotation block at 304–308):
|
||||
|
||||
```sh
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-bot-token --value "<BotFather-token>"
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/gemini-api-key --value "<google-ai-studio-key>"
|
||||
```
|
||||
|
||||
**Failure mode:** A new user copy-pastes the block as-is. The SSM parameter is created with the literal value `<BotFather-token>`. The Lambda then resolves a token that's the string `<BotFather-token>` and Telegram returns 401. The error is hard to debug because the value is `SecureString` — `get-parameter` without `--with-decryption` hides it.
|
||||
|
||||
**Suggested rewrite:** prompt for the values:
|
||||
```sh
|
||||
read -rsp 'BotFather token: ' BOT_TOKEN; echo
|
||||
read -rsp 'Gemini API key: ' GEMINI; echo
|
||||
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-bot-token --value "$BOT_TOKEN"
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/gemini-api-key --value "$GEMINI"
|
||||
|
||||
unset BOT_TOKEN GEMINI
|
||||
```
|
||||
|
||||
This also addresses the shell-history leak: `aws ssm put-parameter --value "<actual-token-here>"` ends up in `~/.bash_history` and in `/proc/<pid>/cmdline` while the CLI runs. Using `read -r` keeps the value out of history; the env var still shows in process listings briefly but only the CLI's own PID owns it.
|
||||
|
||||
---
|
||||
|
||||
## Important (should-fix)
|
||||
|
||||
### I1. `--value "$(openssl rand …)"` leaks to history (lower-severity)
|
||||
|
||||
Lines 136, 140. Even though the generated value is fresh and not a long-term secret in the same way as the bot token, the resulting parameter IS the secret used to authenticate Telegram → webhook and EventBridge → webhook calls. It's saved to shell history as-is once expanded? Actually no — the literal `$(openssl rand -hex 32)` stored in history is harmless because re-running it produces a different value. **However**, the actual generated value briefly appears in `/proc/<pid>/cmdline` of the `aws` process. On a single-user host that's acceptable, but the doc says nothing about it.
|
||||
|
||||
**Suggested rewrite:** generate first, store via env var (same pattern as C5).
|
||||
```sh
|
||||
WEBHOOK_SECRET=$(openssl rand -hex 32)
|
||||
aws ssm put-parameter --profile admin --type SecureString --overwrite \
|
||||
--name /miti99bot/prod/telegram-webhook-secret --value "$WEBHOOK_SECRET"
|
||||
echo "Webhook secret (save somewhere safe): $WEBHOOK_SECRET"
|
||||
unset WEBHOOK_SECRET
|
||||
```
|
||||
|
||||
### I2. OIDC thumbprint is no longer required by AWS
|
||||
|
||||
Line 153: `--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1`
|
||||
|
||||
**Failure mode:** Not a failure — but as of mid-2023, IAM OIDC providers for GitHub Actions validate against root CAs and the thumbprint is ignored. The CLI still requires the flag to be present, so the current value is fine. Worth a one-line note that it's a vestigial parameter (so users don't panic when GitHub rotates intermediate certs).
|
||||
|
||||
**Suggested rewrite:** add a comment:
|
||||
```sh
|
||||
# --thumbprint-list is required by the API but no longer verified by AWS
|
||||
# (IAM now trusts the upstream root CAs for token.actions.githubusercontent.com).
|
||||
```
|
||||
|
||||
### I3. URL concatenation assumes trailing slash
|
||||
|
||||
Lines 219–220:
|
||||
|
||||
```sh
|
||||
URL=… # from previous command
|
||||
curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
|
||||
-d "url=${URL}webhook" \
|
||||
```
|
||||
|
||||
**Failure mode:** Lambda Function URL outputs from CloudFormation are `https://<id>.lambda-url.<region>.on.aws/` (trailing slash always present), so `${URL}webhook` resolves to `…/webhook` — correct. BUT if the user re-types or pastes manually without the trailing slash, it becomes `…webhook` and Telegram POSTs to a non-existent host path. Silent failure.
|
||||
|
||||
**Suggested rewrite:**
|
||||
```sh
|
||||
URL="${URL%/}/" # normalize: ensure exactly one trailing slash
|
||||
curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
|
||||
-d "url=${URL}webhook" ...
|
||||
```
|
||||
|
||||
Or use proper URL building:
|
||||
```sh
|
||||
curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
|
||||
--data-urlencode "url=${URL%/}/webhook" \
|
||||
--data-urlencode "secret_token=$SECRET" \
|
||||
--data-urlencode 'allowed_updates=["message","callback_query"]'
|
||||
```
|
||||
|
||||
`--data-urlencode` is also safer than `-d` for `$SECRET` which is hex-only today but might become base64 (`+/=`) in the future.
|
||||
|
||||
### I4. SSM parameter version pin `:1` will break after first rotation
|
||||
|
||||
`template.yaml` lines 127–130 use `{{resolve:ssm-secure:/miti99bot/${StackEnv}/telegram-bot-token:1}}`. Each `put-parameter --overwrite` bumps the version to 2, 3, 4, ….
|
||||
|
||||
**Failure mode:** Step 2 says `--overwrite`. If a user runs Step 2 twice (e.g. corrects a typo'd bot token before deploying), the parameter version is now 2 but `template.yaml` still resolves `:1`, which now points at the OLD value. The rotating-secrets section (line 307) acknowledges this with "template.yaml pins :1 version; redeploy picks up the new value" — but that comment is WRONG. With version pinned to 1, redeploying does NOT pick up a new value (it picks up version 1 forever). The user must either (a) bump the `:1` to `:2` in template, (b) use no version suffix (latest), or (c) avoid `--overwrite` on the first run.
|
||||
|
||||
**Cross-ref:** This is a `template.yaml` bug as much as a doc bug. The `:1` pin is incompatible with the rotation flow described.
|
||||
|
||||
**Suggested rewrite (doc level):**
|
||||
```sh
|
||||
# WARNING: template.yaml pins SSM version :1. If you run put-parameter
|
||||
# --overwrite, you create version :2 which the template will NOT resolve.
|
||||
# For first-time setup, omit --overwrite. For rotation, either:
|
||||
# (a) drop the :1 pin in template.yaml (resolves latest version), OR
|
||||
# (b) bump the :1 → :N in template.yaml after each put-parameter call.
|
||||
```
|
||||
|
||||
Strongly recommend the template be changed to use `{{resolve:ssm-secure:/miti99bot/${StackEnv}/telegram-bot-token}}` (latest) — but that's out of scope here, just flag it.
|
||||
|
||||
### I5. `IAMFullAccess` and `AmazonS3FullAccess` are over-broad even for bootstrap
|
||||
|
||||
Lines 163–173. SAM does need IAM to create the Lambda execution role, but `IAMFullAccess` lets the CI role create / attach policies to **any** principal in the account — including escalating to admin. `AmazonS3FullAccess` lets CI read/write/delete every bucket in the account.
|
||||
|
||||
**Failure mode:** Compromise of the GitHub Actions runner (malicious dependency, leaked OIDC trust) = account takeover.
|
||||
|
||||
**Suggested narrower starter set:**
|
||||
- Replace `IAMFullAccess` with an inline policy allowing only `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:PassRole`, `iam:GetRole`, `iam:DeleteRole`, `iam:PutRolePolicy`, etc. scoped to `arn:aws:iam::<acct>:role/miti99bot-aws-port-*`.
|
||||
- Replace `AmazonS3FullAccess` with policy allowing only the SAM-managed bucket (`aws-sam-cli-managed-default-samclisourcebucket-*` and the deploy bucket created with `resolve_s3 = true`).
|
||||
- `AmazonEventBridgeFullAccess` covers all rules in the account; scope to `arn:aws:scheduler:<region>:<acct>:schedule/default/miti99bot-*` once Phase 04 schedules exist.
|
||||
- `AWSBudgetsActionsWithAWSResourceControlAccess` is unrelated to deploying — it's for Budgets-triggered IAM actions. Probably not needed for the CFN budget resource (which only needs `budgets:CreateBudget` / `ModifyBudget`). Consider dropping or replacing with `AWSBudgetsReadOnlyAccess` + targeted write perms.
|
||||
|
||||
At minimum the doc should call this out as a starter step, not a "Phase 06 problem":
|
||||
```sh
|
||||
# These 10 managed policies grant near-admin to the GitHub Actions role.
|
||||
# AT MINIMUM, before merging to main:
|
||||
# - Replace IAMFullAccess with a role-prefix-scoped inline policy.
|
||||
# - Replace AmazonS3FullAccess with the SAM-managed bucket only.
|
||||
# See Step 7 for the long-term plan.
|
||||
```
|
||||
|
||||
### I6. `--guided` deploy will create an S3 bucket with no lifecycle / versioning policy
|
||||
|
||||
`samconfig.toml` has `resolve_s3 = true`. `sam deploy --guided` and subsequent deploys upload artifacts to a SAM-managed bucket (`aws-sam-cli-managed-default-samclisourcebucket-*`). By default the bucket has versioning ENABLED (SAM creates it that way) and no lifecycle rule — old packaged Lambda zips accumulate indefinitely.
|
||||
|
||||
**Failure mode:** Free tier on S3 is 5 GB. A 30 MB Lambda zip × 200 deploys = 6 GB. Bot leaves free tier silently.
|
||||
|
||||
**Suggested rewrite:** add a note + lifecycle policy:
|
||||
```sh
|
||||
# After first deploy, find the SAM-managed bucket and add a 30-day lifecycle
|
||||
# rule to expire old artifacts:
|
||||
BUCKET=$(aws s3 ls --profile admin | awk '/aws-sam-cli-managed-default/ {print $3}')
|
||||
aws s3api put-bucket-lifecycle-configuration --profile admin --bucket "$BUCKET" \
|
||||
--lifecycle-configuration '{"Rules":[{"ID":"expire-old-deploys","Status":"Enabled","Filter":{},"Expiration":{"Days":30},"NoncurrentVersionExpiration":{"NoncurrentDays":7},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":1}}]}'
|
||||
```
|
||||
|
||||
### I7. `pip install awscli` ships v1 — `aws ssm get-parameter --query` works the same, but worth verifying
|
||||
|
||||
Line 50: `aws --version # aws-cli/1.x (pip ships v1; v2 is not on PyPI)`
|
||||
|
||||
This is correct and the doc calls it out. v1 supports `--query`, `--output`, `--profile`, `--with-decryption`. The `get-parameter` invocations on lines 213, 216 are valid for v1.
|
||||
|
||||
**Watch:** AWS CLI v1 is in maintenance mode (security only, since 2023). The doc could note an EOL risk but it's low priority.
|
||||
|
||||
### I8. CloudWatch Logs retention is set in template (good) but doc claims 7 days unconditionally
|
||||
|
||||
Doc line 20 and 25: "log retention pinned to 7 days". `template.yaml` line 86 confirms `RetentionInDays: 7`. ✓ Correct.
|
||||
|
||||
### I9. X-Ray "Active" tracing free-tier monitoring not surfaced
|
||||
|
||||
Doc line 25 mentions the 100k traces/mo cap. `template.yaml` line 52 sets `Tracing: Active` globally. No alarm or check is provided. For a free-tier-strict doc, a one-liner reminder:
|
||||
|
||||
```sh
|
||||
# Monthly X-Ray traces metric (free tier 100k/mo):
|
||||
aws cloudwatch get-metric-statistics --namespace AWS/X-Ray \
|
||||
--metric-name TracesProcessed --start-time $(date -u -d '30 days ago' +%FT%TZ) \
|
||||
--end-time $(date -u +%FT%TZ) --period 2592000 --statistics Sum
|
||||
```
|
||||
|
||||
### I10. `aws cloudformation describe-stacks --query "...[?...]..."` — JMESPath quoting
|
||||
|
||||
Lines 201–204:
|
||||
|
||||
```sh
|
||||
aws cloudformation describe-stacks --profile admin \
|
||||
--stack-name miti99bot-aws-port \
|
||||
--query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text
|
||||
```
|
||||
|
||||
**Correctness:** valid JMESPath. Returns a single-line `https://….lambda-url.ap-southeast-1.on.aws/` with `--output text`. ✓ Works.
|
||||
|
||||
**Minor:** if the stack doesn't have a `FunctionUrl` output (wrong stack name), `--output text` returns empty string with exit 0. User then sets `URL=""` and silently sends a webhook URL of `webhook` to Telegram. Worth wrapping:
|
||||
```sh
|
||||
URL=$(aws cloudformation describe-stacks ...)
|
||||
[ -n "$URL" ] || { echo "FunctionUrl output not found — did the stack deploy?"; exit 1; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Minor / Suggestions
|
||||
|
||||
### M1. `make sam-deploy` in rotation step skips `--profile admin`
|
||||
|
||||
Line 307: `make sam-deploy # template.yaml pins :1 version; redeploy picks up the new value`
|
||||
|
||||
The Makefile target `sam-deploy` does not set `AWS_PROFILE`. After Step 7 the user has rotated/deleted the `admin` keys — fine, CI handles it. But during early bootstrap (before GH Actions is wired), `make sam-deploy` without `AWS_PROFILE=admin` will use the user's default profile, which may not exist.
|
||||
|
||||
**Suggested rewrite:**
|
||||
```sh
|
||||
AWS_PROFILE=admin make sam-deploy # or rely on CI after Step 7
|
||||
```
|
||||
|
||||
### M2. `read -rsp` portability
|
||||
|
||||
The suggestions above using `read -rsp` work on bash (Linux Ubuntu 24.04 — the doc's stated target). `dash` does not support `-s`. The doc explicitly targets bash on Ubuntu, so this is fine; flag only as a portability footnote.
|
||||
|
||||
### M3. `dpkg --print-architecture` is Debian-only
|
||||
|
||||
Line 92: GH CLI install uses `dpkg --print-architecture`. Doc targets Ubuntu 24.04, so OK. Portability note only.
|
||||
|
||||
### M4. `sudo tar -C /usr/local -xzf /tmp/go.tgz` after `sudo rm -rf /usr/local/go`
|
||||
|
||||
Lines 79–80. The `rm -rf /usr/local/go` is acceptable here (specific path, not interpolated). If a future revision changes this to `sudo rm -rf /usr/local/$GO_DIR` and `GO_DIR` is empty, it deletes `/usr/local`. Guard against future regressions:
|
||||
```sh
|
||||
[ -n "$GO_VERSION" ] || { echo "GO_VERSION unset"; exit 1; }
|
||||
```
|
||||
|
||||
Low priority — current code is fine.
|
||||
|
||||
### M5. `newgrp docker` (line 103) only affects the current shell
|
||||
|
||||
User completes Docker install, then proceeds to other steps in the same shell where `newgrp docker` was run. If they open a new terminal, they'll hit "permission denied" on docker until next login. Worth a one-line note:
|
||||
```sh
|
||||
# newgrp docker only affects this shell; log out + back in to make it permanent
|
||||
```
|
||||
|
||||
### M6. `template.yaml` uses `provided.al2023` + `Handler: bootstrap` + ARM64 — doc doesn't explicitly state the binary must be named `bootstrap`
|
||||
|
||||
The Makefile builds `build/lambda/bootstrap`. Template references it. If a user customizes the Makefile to output `build/lambda/server` or similar, the deploy succeeds but invocation fails at runtime with `init error: fork/exec /var/task/bootstrap: no such file`.
|
||||
|
||||
**Suggested rewrite (one line):**
|
||||
```sh
|
||||
# Lambda's provided.al2023 runtime expects an executable named exactly `bootstrap`
|
||||
# in the deployment package root. Don't rename the make target.
|
||||
```
|
||||
|
||||
### M7. `Architectures: [arm64]` + `LambdaAdapterLayerArm64:25` — region-pinned
|
||||
|
||||
`template.yaml` line 35: `arn:aws:lambda:ap-southeast-1:753240598075:layer:LambdaAdapterLayerArm64:25`. If a user changes `region` in `samconfig.toml`, this layer ARN no longer exists in the new region.
|
||||
|
||||
**Suggested rewrite:** doc should warn (currently it just says "Change in samconfig.toml if needed" without flagging the layer ARN):
|
||||
```sh
|
||||
# Changing region also requires updating LambdaAdapterLayerArn in template.yaml
|
||||
# (each region publishes its own ARN). See:
|
||||
# https://github.com/awslabs/aws-lambda-web-adapter/releases
|
||||
```
|
||||
|
||||
### M8. `sam deploy --guided` prompts (Step 4) don't list all required answers
|
||||
|
||||
Doc lists: stack name, region, capabilities, save samconfig. Real prompts include:
|
||||
- "Confirm changes before deploy [y/N]" → recommend `N` (matches CI)
|
||||
- "Allow SAM CLI IAM role creation [Y/n]" → must be `Y`
|
||||
- "Disable rollback [y/N]" → must be `N` (free-tier safety: auto-rollback prevents stuck broken stacks)
|
||||
- "Save arguments to configuration file [Y/n]" → `Y`
|
||||
- "SAM configuration file" → accept default
|
||||
- "SAM configuration environment" → accept default
|
||||
|
||||
Worth listing the full sequence so a new user doesn't accidentally `y` to "Disable rollback".
|
||||
|
||||
### M9. `aws iam attach-role-policy` order of arguments
|
||||
|
||||
Lines 174–175 — correct, but `--profile admin` after `--role-name` works the same regardless of order. Cosmetic only.
|
||||
|
||||
### M10. Free-tier table claims DynamoDB request free tier of 200M (line 274)
|
||||
|
||||
Doc table line 274 says "DynamoDB req 200M". AWS DDB free tier is 25 GB storage + 25 WCU / 25 RCU provisioned-capacity-equivalent always-free (which is approx 200M req/mo if you saturate at 25 RCU). PAY_PER_REQUEST conversion: 25 RCU ≈ 65M strongly-consistent reads + 25 WCU ≈ 65M writes. The "200M" figure conflates request types. Worth a tiny clarification:
|
||||
```
|
||||
| DynamoDB on-demand req | ≈ 50M reads + ≈ 50M writes (varies by item size) | ... |
|
||||
```
|
||||
|
||||
Cosmetic. The directional warning (5% = runaway) is correct.
|
||||
|
||||
### M11. `dd if=… of=/usr/share/keyrings/githubcli-archive-keyring.gpg`
|
||||
|
||||
Line 90. `dd` here is acting as a glorified `cp` but with no useful flag. Common pattern, works fine. Cosmetic.
|
||||
|
||||
---
|
||||
|
||||
## Cross-references with `template.yaml`
|
||||
|
||||
| Free-tier claim in doc | Verified in template? |
|
||||
|---|---|
|
||||
| "PITR disabled" | ✓ line 74 |
|
||||
| "Retention 7 days" | ✓ line 86 |
|
||||
| "ARM64" | ✓ line 49 |
|
||||
| "PAY_PER_REQUEST" | ✓ line 66 |
|
||||
| "Function URL (not API Gateway)" | ✓ line 131–137 |
|
||||
| "X-Ray active tracing" | ✓ line 52 |
|
||||
| "$1 budget alarm" | ✓ line 180–203 (conditional on AlertEmail) |
|
||||
| "Memory 256 MB" | ✓ line 50 |
|
||||
|
||||
All free-tier claims are backed by `template.yaml`. No drift.
|
||||
|
||||
---
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
1. Should `template.yaml`'s `:1` SSM version pin be dropped to fix the rotation contradiction (I4)? Doc and template currently disagree on rotation flow.
|
||||
2. Is the `cd()` override (C4) load-bearing for the user's actual workflow, or just a convenience hint? If just convenience, drop it; if some downstream `make` target requires the venv-on-`cd`, surface that requirement instead.
|
||||
3. The repo has `aws/README.md` with mostly-overlapping content. Should this guide supersede it, or stay as a "human-readable" wrapper? If both stay, keep them in sync (rotation contradiction exists in both).
|
||||
4. Is `provided.al2023` the long-term runtime, or is the team planning to switch to `provided.al2` (older) for AL2-LTS support? Affects whether to lock the doc to AL2023-specific behavior.
|
||||
5. The IAM trust policy at `aws/iam-github-oidc-trust.json` is checked in with a real account ID (`225603493174`) and `tiennm99/miti99bot` — is that intentional (this repo's owner), or should it be templated with placeholders? Doc says "Edit … fill in your 12-digit AWS account ID" but the file is already filled in.
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
|
||||
Concerns: C1 (rollback command incorrect) and C4 (`cd()` override) need real fixes, not just clarification. C5 (literal placeholders) and I4 (SSM `:1` pin vs `--overwrite` contradiction) will burn new users on first run. I5 (IAM blast radius) is a long-term security finding but not a copy-paste hazard.
|
||||
|
||||
Sources:
|
||||
- [update-stack — AWS CLI Command Reference](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/update-stack.html)
|
||||
- [Continue rolling back an update — AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-continueupdaterollback.html)
|
||||
- [continue-update-rollback — AWS CLI Command Reference](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/continue-update-rollback.html)
|
||||
@@ -0,0 +1,282 @@
|
||||
# AWS CLI Command Verification Report
|
||||
|
||||
**Date:** 2025-05-13
|
||||
**Scope:** `deploy-aws-free-tier-guide.md` cross-reference with live AWS docs (Feb 2025 cutoff + current)
|
||||
**Target Environment:** Ubuntu 24.04 ARM64, AWS CLI v1 (pip), SAM CLI latest, region `ap-southeast-1`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Most commands in the guide are **correct and functional**. Identified:
|
||||
- **6 items CORRECT** (verified against current AWS docs)
|
||||
- **4 items NEEDS CORRECTION** (inaccurate claims or outdated values)
|
||||
- **2 items OUTDATED/DEPRECATED** (breaking changes coming; action needed)
|
||||
- **1 item UNRESOLVABLE** (Telegram endpoint behavior, best-effort verification)
|
||||
|
||||
**Critical Issue:** DynamoDB free-tier claim is **inaccurate**. Doc claims "25 GiB + 25 RCU/WCU **always-free** for on-demand" but 25 RCU/WCU only apply to **provisioned mode**, not on-demand. Document uses on-demand (`PAY_PER_REQUEST`) throughout, so the free units don't apply.
|
||||
|
||||
**Layer Version Drift:** Lambda Web Adapter ARN pinned to `:25`, but search shows both `:24` and `:25` exist; `:25` is newer but verify in target region.
|
||||
|
||||
**AWS CLI v1 Deprecation:** Enters maintenance mode **July 15, 2026**; reaches EOL **July 15, 2027**. Not urgent now, but document this.
|
||||
|
||||
---
|
||||
|
||||
## Confirmed Correct
|
||||
|
||||
### 1. AWS CLI v1 via pip
|
||||
**Line 50:** "aws-cli/1.x (pip ships v1; v2 is not on PyPI)"
|
||||
|
||||
✅ **CORRECT**
|
||||
- PyPI's `awscli` package is indeed v1. v2 is distributed only as a standalone bundle, not via pip.
|
||||
- v1 supports all commands used in guide: `ssm`, `iam`, `cloudformation`, `lambda`, `logs`, `cloudwatch`.
|
||||
- v1 will enter maintenance mode July 15, 2026; EOL July 15, 2027.
|
||||
|
||||
**Source:** [CLI v1 Maintenance Mode Announcement](https://aws.amazon.com/blogs/developer/cli-v1-maintenance-mode-announcement/)
|
||||
|
||||
---
|
||||
|
||||
### 2. GitHub OIDC thumbprint
|
||||
**Line 153:** `--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1`
|
||||
|
||||
✅ **CORRECT**
|
||||
- Thumbprint `6938fd4d98bab03faadb97b34396831e3780aea1` is valid and widely used for GitHub Actions OIDC integration.
|
||||
- GitHub has two cross-signed intermediary certs; AWS now allows both. Thumbprint validation is still required by AWS but GitHub has added cert to AWS root store (Dec 2024+).
|
||||
- AWS Terraform provider (via Go SDK update Dec 2024) now makes thumbprints optional; however, AWS CLI still requires it.
|
||||
- **No action needed** — thumbprint works fine for CLI.
|
||||
|
||||
**Source:** [GitHub Actions OIDC Update for Terraform and AWS](https://colinbarker.me.uk/blog/2025-01-12-github-actions-oidc-update/)
|
||||
|
||||
---
|
||||
|
||||
### 3. Lambda always-free tier (compute)
|
||||
**Line 15:** "1M req + 400k GB-s / mo, **always-free**"
|
||||
|
||||
✅ **CORRECT**
|
||||
- 1 million requests per month + 400,000 GB-seconds per month is always-free (never expires).
|
||||
- Applies to all Lambda invocations regardless of region.
|
||||
|
||||
**Source:** [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/)
|
||||
|
||||
---
|
||||
|
||||
### 4. SSM Parameter SecureString tier default
|
||||
**Lines 130, 133–140:** "Parameter Store Standard tier is free; SecureString uses the AWS-managed KMS key, also free."
|
||||
|
||||
✅ **CORRECT** (with minor clarification)
|
||||
- Default tier for SecureString is **Standard** (4 KB limit, free).
|
||||
- AWS-managed KMS key (aws/ssm) is **no additional cost**.
|
||||
- No `--tier` flag in commands → defaults to Standard, which is correct for this use.
|
||||
- Advanced tier ($0.05/param/month) only needed for 8 KB params; doc doesn't use it.
|
||||
|
||||
**Source:** [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
|
||||
|
||||
---
|
||||
|
||||
### 5. Lambda runtime `provided.al2023` with ARM64
|
||||
**From context:** Guide uses `provided.al2023` runtime for Go ARM64.
|
||||
|
||||
✅ **CORRECT**
|
||||
- `provided.al2023` is GA and recommended for Go on ARM64.
|
||||
- AWS recommends this over deprecated `go1.x`.
|
||||
- Supports both x86_64 and arm64; executable should be named `bootstrap`, built with `GOARCH=arm64`.
|
||||
|
||||
**Source:** [Introducing Amazon Linux 2023 runtime for AWS Lambda](https://aws.amazon.com/blogs/compute/introducing-the-amazon-linux-2023-runtime-for-aws-lambda/)
|
||||
|
||||
---
|
||||
|
||||
### 6. Telegram Bot API `setWebhook` secret_token
|
||||
**Lines 219–222:** Uses `secret_token` parameter in `setWebhook` call.
|
||||
|
||||
✅ **CORRECT**
|
||||
- `secret_token` parameter is valid (1-256 chars, alphanumeric + `-` + `_`).
|
||||
- Telegram includes `X-Telegram-Bot-Api-Secret-Token` header in webhook requests.
|
||||
- Response format and behavior match current Telegram Bot API spec.
|
||||
|
||||
**Source:** [Telegram Bot API Documentation](https://core.telegram.org/bots/api)
|
||||
|
||||
---
|
||||
|
||||
## Needs Correction
|
||||
|
||||
### 1. DynamoDB Free Tier (CRITICAL — Line 17)
|
||||
**Current text:** "DynamoDB (PAY_PER_REQUEST) | 25 GiB + 25 RCU/WCU **always-free** | far below"
|
||||
|
||||
❌ **INACCURATE**
|
||||
**Issue:** Doc uses on-demand mode (`PAY_PER_REQUEST`) throughout the guide. However:
|
||||
- **25 GiB storage** = always-free for on-demand ✅
|
||||
- **25 RCU/WCU** = **only for provisioned mode**, NOT on-demand ❌
|
||||
|
||||
On-demand mode has **no free request capacity**. You pay per request (approx $1.25 per million read requests, $6.25 per million write requests).
|
||||
|
||||
**Severity:** MEDIUM — Misleading users about cost structure. Bot's real usage likely stays under $0.01/mo, but the free-tier claim is wrong.
|
||||
|
||||
**Doc line 17 & watch-table line 274:**
|
||||
```
|
||||
| DynamoDB (PAY_PER_REQUEST) | 25 GiB + 25 RCU/WCU **always-free**
|
||||
| DynamoDB req | 200M | Past 5% (sign of runaway loop)
|
||||
```
|
||||
|
||||
**Correction:**
|
||||
```
|
||||
| DynamoDB (PAY_PER_REQUEST) | 25 GiB storage **always-free**; requests charged per-call | far below
|
||||
| DynamoDB req (on-demand) | Charged per request (~$1.25/M reads); no free tier | Far below $0.01/mo
|
||||
```
|
||||
|
||||
**Source:** [Amazon DynamoDB Pricing](https://aws.amazon.com/dynamodb/pricing/), [DynamoDB Free Tier Guide](https://dynobase.dev/dynamodb-free-tier/)
|
||||
|
||||
---
|
||||
|
||||
### 2. CloudFormation `--use-previous-template` (CRITICAL — Line 289)
|
||||
**Current text:** "To roll back a successful-but-bad deploy: `aws cloudformation update-stack --stack-name miti99bot-aws-port --use-previous-template --capabilities CAPABILITY_IAM`"
|
||||
|
||||
❌ **INACCURATE**
|
||||
**Issue:** `--use-previous-template` does **NOT rollback**. It re-deploys the most-recent template with no changes. From AWS docs:
|
||||
|
||||
> "If you haven't modified the stack template, select **Use existing template**"
|
||||
|
||||
This is equivalent to hitting "Update" without changing the template. CloudFormation already **auto-rolls-back failed updates** (line 286 is correct). But for a successful-but-bad deploy, `--use-previous-template` won't help.
|
||||
|
||||
**True rollback options:**
|
||||
1. **During UPDATE_IN_PROGRESS:** `aws cloudformation cancel-update-stack`
|
||||
2. **After UPDATE_ROLLBACK_COMPLETE or UPDATE_ROLLBACK_FAILED:** `aws cloudformation continue-update-rollback`
|
||||
3. **Redeploy from known-good git SHA** (line 294–297, already in doc ✅)
|
||||
|
||||
**Severity:** HIGH — Users will not successfully rollback using this command.
|
||||
|
||||
**Doc lines 289–292:**
|
||||
```sh
|
||||
aws cloudformation update-stack \
|
||||
--stack-name miti99bot-aws-port \
|
||||
--use-previous-template --capabilities CAPABILITY_IAM
|
||||
```
|
||||
|
||||
**Correction:**
|
||||
```sh
|
||||
# For an ongoing update, cancel it:
|
||||
aws cloudformation cancel-update-stack --stack-name miti99bot-aws-port
|
||||
|
||||
# Or, redeploy from a known-good commit:
|
||||
git checkout <good-sha>
|
||||
make sam-deploy
|
||||
```
|
||||
|
||||
**Source:** [Update stacks directly — AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-direct.html)
|
||||
|
||||
---
|
||||
|
||||
### 3. Lambda Web Adapter Layer Version (Line 25 in template)
|
||||
**Assumption from context:** SAM template likely uses:
|
||||
```
|
||||
arn:aws:lambda:ap-southeast-1:753240598075:layer:LambdaAdapterLayerArm64:25
|
||||
```
|
||||
|
||||
⚠️ **VERSION DRIFT**
|
||||
**Issue:** Search results confirm both `:24` and `:25` exist for arm64 in ap-southeast-1. The doc/template pin is to `:25`, which is the **newer version**. However:
|
||||
- Version `:24` is also functional
|
||||
- Version `:25` is recommended
|
||||
- No official deprecation of `:24` stated
|
||||
|
||||
**Action:** `:25` is correct and current. No immediate fix needed, but monitor AWS Labs releases for future versions.
|
||||
|
||||
**Severity:** LOW — Current version works. Verify in `ap-southeast-1` region before deploy.
|
||||
|
||||
**Source:** [AWS Lambda Web Adapter GitHub](https://github.com/aws/aws-lambda-web-adapter), [Layer version tracking](https://github.com/awslabs/aws-lambda-web-adapter)
|
||||
|
||||
---
|
||||
|
||||
### 4. EventBridge Scheduler + Lambda (Potential clarification)
|
||||
**Context:** Doc assumes Scheduler can invoke Lambda via Function URL with custom HTTP headers.
|
||||
|
||||
⚠️ **PARTIALLY ADDRESSED**
|
||||
**Issue:** AWS docs show EventBridge Scheduler can:
|
||||
- Invoke Lambda with `lambda:InvokeFunction` (standard async invocation) ✅
|
||||
- Target HTTP endpoints with custom headers (HTTPS target type) ✅
|
||||
|
||||
However, combining these (Function URL + custom Cron header via Scheduler) is **not explicitly documented in AWS examples**.
|
||||
|
||||
**Workaround:** Use Scheduler → Lambda → HTTP POST to Function URL, or Scheduler → HTTP target directly with Function URL.
|
||||
|
||||
**Severity:** LOW — Likely works, but best-practice is to invoke Lambda directly via Scheduler, not via Function URL.
|
||||
|
||||
**Source:** [Invoke Lambda on a schedule](https://docs.aws.amazon.com/lambda/latest/dg/with-eventbridge-scheduler.html)
|
||||
|
||||
---
|
||||
|
||||
## Outdated Information
|
||||
|
||||
### 1. AWS CLI v1 Deprecation Timeline (Advisory)
|
||||
**Line 50, 64:** Doc mentions v1 is on PyPI and usable.
|
||||
|
||||
⚠️ **UPCOMING DEPRECATION**
|
||||
- v1 **enters maintenance mode: July 15, 2026** (1+ year away, no urgent action)
|
||||
- v1 **EOL: July 15, 2027**
|
||||
|
||||
**Recommendation:** Add a note in docs advising users to plan migration to v2 within 12 months.
|
||||
|
||||
**Source:** [AWS CLI v1 Maintenance Mode Announcement](https://aws.amazon.com/blogs/developer/cli-v1-maintenance-mode-announcement/)
|
||||
|
||||
---
|
||||
|
||||
### 2. SAM CLI Guided Prompts (Minor drift possible)
|
||||
**Lines 192–196:** Doc lists prompts from `sam deploy --guided`.
|
||||
|
||||
⚠️ **VERSION-DEPENDENT**
|
||||
SAM CLI versions 1.x+ maintain consistent prompts, but newer releases may rename/reorder them. Current prompts include:
|
||||
- Stack name
|
||||
- Region
|
||||
- Confirm changes
|
||||
- IAM role creation
|
||||
- Rollback config
|
||||
- Save to samconfig.toml
|
||||
|
||||
**Action:** If SAM is updated, re-run `sam deploy --guided --help` to verify prompts match doc.
|
||||
|
||||
**Severity:** VERY LOW — Functional impact is minimal; just prompts may differ.
|
||||
|
||||
**Source:** [AWS SAM Deployment Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli-deploy.html)
|
||||
|
||||
---
|
||||
|
||||
## Verified Correct (No Issues)
|
||||
|
||||
- **Lambda Function URL AuthType values:** `AWS_IAM`, `NONE` ✅
|
||||
- **Lambda Function URL InvokeMode values:** `BUFFERED`, `RESPONSE_STREAM` ✅
|
||||
- **SSM get-parameters-by-path syntax:** `--with-decryption`, `--query`, pagination control all correct ✅
|
||||
- **AWS Budgets resource type:** Supports BudgetLimit with Unit: "USD", Amount: "1" ✅
|
||||
- **AWS account ID 753240598075:** Confirmed as official AWS Labs publisher for Lambda Web Adapter layer ✅
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [AWS CLI v1 Maintenance Mode Announcement](https://aws.amazon.com/blogs/developer/cli-v1-maintenance-mode-announcement/)
|
||||
- [GitHub Actions OIDC Update for Terraform and AWS](https://colinbarker.me.uk/blog/2025-01-12-github-actions-oidc-update/)
|
||||
- [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/)
|
||||
- [Amazon DynamoDB Pricing](https://aws.amazon.com/dynamodb/pricing/)
|
||||
- [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
|
||||
- [Introducing Amazon Linux 2023 runtime for AWS Lambda](https://aws.amazon.com/blogs/compute/introducing-the-amazon-linux-2023-runtime-for-aws-lambda/)
|
||||
- [Telegram Bot API Documentation](https://core.telegram.org/bots/api)
|
||||
- [Update stacks directly — AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-direct.html)
|
||||
- [AWS SAM Deployment Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli-deploy.html)
|
||||
- [Invoke Lambda on a schedule](https://docs.aws.amazon.com/lambda/latest/dg/with-eventbridge-scheduler.html)
|
||||
- [AWS Budgets::Budget CloudFormation Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-budgets-budget.html)
|
||||
- [AWS Lambda Web Adapter GitHub](https://github.com/aws/aws-lambda-web-adapter)
|
||||
|
||||
---
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
1. **EventBridge Scheduler + Function URL with custom headers:** Is invoking a Lambda Function URL from EventBridge Scheduler with cron-triggered custom headers (e.g., `X-Cron-Secret`) officially supported, or should users invoke Lambda directly then POST to Function URL? Docs suggest both are possible but don't explicitly cover the combo.
|
||||
|
||||
2. **DynamoDB on-demand actual free capacity:** Doc claims "far below" quota, but has the bot's actual usage been measured? Actual RCU/WCU cost on on-demand depends on read patterns. Verify via CloudWatch metrics or cost explorer post-deploy.
|
||||
|
||||
3. **Lambda Web Adapter version update cadence:** How frequently does AWS Labs release new versions (`:24` → `:25` → `:26`)? Should the template auto-pin to latest or pin to a stable version?
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
|
||||
**Summary:** 2 critical issues found (DynamoDB free-tier claim, CloudFormation rollback command) that need doc updates. 4 other minor findings (version drift, deprecation timeline, optional clarifications). All AWS CLI commands are functional on v1; no breaking command-level changes detected.
|
||||
|
||||
**Concerns:** DynamoDB cost claim is actively misleading; CloudFormation rollback won't work as documented. Recommend fixing these before next deployment guide release.
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
version = 0.1
|
||||
|
||||
[default.global.parameters]
|
||||
stack_name = "miti99bot-aws-port"
|
||||
stack_name = "miti99bot"
|
||||
|
||||
[default.deploy.parameters]
|
||||
region = "ap-southeast-1"
|
||||
@@ -9,7 +9,7 @@ capabilities = "CAPABILITY_IAM"
|
||||
confirm_changeset = false
|
||||
fail_on_empty_changeset = false
|
||||
resolve_s3 = true
|
||||
s3_prefix = "miti99bot-aws-port"
|
||||
s3_prefix = "miti99bot"
|
||||
# Secrets MUST live in SSM Parameter Store (see aws/README.md). Never put
|
||||
# them here — this file is committed.
|
||||
parameter_overrides = [
|
||||
|
||||
+3
-4
@@ -2,7 +2,7 @@ AWSTemplateFormatVersion: '2010-09-09'
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
|
||||
Description: >
|
||||
miti99bot-go: Telegram bot on AWS Lambda (Go ZIP + LWA) with DynamoDB KV
|
||||
miti99bot: Telegram bot on AWS Lambda (Go ZIP + LWA) with DynamoDB KV
|
||||
+ EventBridge cron + SSM Parameter Store secrets. Strict free-tier deploy.
|
||||
|
||||
Parameters:
|
||||
@@ -171,9 +171,8 @@ Resources:
|
||||
Action: sqs:SendMessage
|
||||
Resource: !GetAtt CronDLQ.Arn
|
||||
|
||||
# NOTE: Concrete schedules are added in Phase 04. We provision the role +
|
||||
# DLQ here so the IaC review surface stays accurate and Phase 04 just adds
|
||||
# AWS::Scheduler::Schedule resources.
|
||||
# Concrete AWS::Scheduler::Schedule resources are added per cron handler;
|
||||
# the role + DLQ above are provisioned once and reused across all schedules.
|
||||
|
||||
# --- Cost guard -----------------------------------------------------------
|
||||
|
||||
|
||||
Reference in new issue
Block a user