From 03c247661669395674ca95ec99d45e55e6d7d965 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 10 May 2026 02:29:52 +0700 Subject: [PATCH] docs(aws): bootstrap + ops cheatsheets - aws/README.md: one-time AWS account setup (account ID, IAM, OIDC) - aws/iam-github-oidc-trust.json: GitHub OIDC trust policy template - docs/deploy-aws.md: steady-state deployment operations guide --- aws/README.md | 133 ++++++++++++++++++++++++++++++ aws/iam-github-oidc-trust.json | 24 ++++++ docs/deploy-aws.md | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 aws/README.md create mode 100644 aws/iam-github-oidc-trust.json create mode 100644 docs/deploy-aws.md diff --git a/aws/README.md b/aws/README.md new file mode 100644 index 0000000..427e4c9 --- /dev/null +++ b/aws/README.md @@ -0,0 +1,133 @@ +# AWS bootstrap commands + +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. + +> **Region:** `ap-southeast-1` (Singapore). Change in `samconfig.toml` if needed. +> **Stack name:** `miti99bot-aws-port`. 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`. +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 + ``` + +## 2. SSM Parameter Store secrets + +Create the four required secrets. **Names must match `template.yaml`** (`/miti99bot/${StackEnv}/…`). + +```sh +aws ssm put-parameter --name /miti99bot/prod/telegram-bot-token \ + --value "" --type SecureString --profile bootstrap-admin + +aws ssm put-parameter --name /miti99bot/prod/telegram-webhook-secret \ + --value "$(openssl rand -hex 32)" --type SecureString --profile bootstrap-admin + +aws ssm put-parameter --name /miti99bot/prod/gemini-api-key \ + --value "" --type SecureString --profile bootstrap-admin + +aws ssm put-parameter --name /miti99bot/prod/cron-shared-secret \ + --value "$(openssl rand -hex 32)" --type SecureString --profile bootstrap-admin +``` + +Save the webhook + cron secrets locally — you'll set them on the Telegram side and on the EventBridge schedule headers. + +## 3. GitHub OIDC identity provider + +One-time per AWS account: + +```sh +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 +``` + +(GitHub publishes the canonical thumbprint; verify on docs.github.com if rotated.) + +## 4. Deploy IAM role for GitHub Actions + +Edit `aws/iam-github-oidc-trust.json` to set your AWS account ID and GitHub repo, then: + +```sh +aws iam create-role \ + --role-name github-deploy-miti99bot \ + --assume-role-policy-document file://aws/iam-github-oidc-trust.json \ + --profile bootstrap-admin + +# Permissions (broad to start; tighten in Phase 06). +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 --role-name github-deploy-miti99bot \ + --policy-arn "$arn" --profile bootstrap-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. + +## 5. Add GitHub repo secrets + +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 | + +`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) + +```sh +make build-lambda +sam build +AWS_PROFILE=bootstrap-admin sam deploy --guided +``` + +Confirm: +- Stack name: `miti99bot-aws-port` +- 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 +``` +Note the `FunctionUrl` — Phase 07 sets the Telegram webhook to it. + +## 7. Tighten — optional but recommended + +Once the first deploy succeeds: +1. Rotate / delete `bootstrap-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. + +--- + +## Lambda Web Adapter layer ARN + +Pinned in `template.yaml` parameter `LambdaAdapterLayerArn`. Bump by checking: +- https://github.com/awslabs/aws-lambda-web-adapter/releases (look at the `Releases` page for the latest layer version) +- Format: `arn:aws:lambda:ap-southeast-1:753240598075:layer:LambdaAdapterLayerArm64:` + +## Cost expectations + +After the stack is up but idle, monthly cost should be **$0**. If you ever see >$0.01 in Cost Explorer, investigate — most likely culprits: CloudWatch Logs ingestion volume, DynamoDB writes from a runaway loop, or accidental egress past the 100 GB free tier. diff --git a/aws/iam-github-oidc-trust.json b/aws/iam-github-oidc-trust.json new file mode 100644 index 0000000..a47eaf1 --- /dev/null +++ b/aws/iam-github-oidc-trust.json @@ -0,0 +1,24 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::REPLACE_WITH_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "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" + ] + } + } + } + ] +} diff --git a/docs/deploy-aws.md b/docs/deploy-aws.md new file mode 100644 index 0000000..a428d23 --- /dev/null +++ b/docs/deploy-aws.md @@ -0,0 +1,142 @@ +# Deploy: AWS (Lambda + DynamoDB + EventBridge) + +This is the production deploy path for `miti99bot-go`. Strict free-tier targets, region `ap-southeast-1`. + +> **First-time setup:** see `aws/README.md`. This doc is for steady-state operations. + +## Architecture (one diagram) + +``` +Telegram ──HTTPS──► Lambda Function URL (AuthType: NONE) + └─► AWS Lambda Web Adapter ──► localhost:8080 + └─► Go http.Handler (cmd/server) + ├─► DynamoDB (KV) + ├─► Gemini API (AI modules) + └─► Telegram Bot API (replies) + +EventBridge Scheduler ──cron──► HTTPS POST /cron/{name} + + Header X-Cron-Token (from SSM) +``` + +## Deploy + +### Via GitHub Actions (canonical) + +``` +git push origin main +``` + +Triggers `.github/workflows/deploy.yml`: +1. OIDC assume `github-deploy-miti99bot` role +2. `make build-lambda` (Go ARM64 ZIP-ready binary) +3. `sam build && sam deploy` +4. Smoke `curl /` + +### Manual (emergency / staging) + +```sh +make build-lambda +make sam-deploy # uses samconfig.toml defaults +ALERT_EMAIL=you@example.com make sam-deploy # with budget alert wired +``` + +## Verify + +```sh +make logs SINCE=10m + +aws cloudformation describe-stacks --stack-name miti99bot-aws-port \ + --query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text + +curl -fsSL "$(...)/" | jq . # health JSON +``` + +## Set the Telegram webhook (Phase 07) + +```sh +URL=$(aws cloudformation describe-stacks --stack-name miti99bot-aws-port \ + --query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" --output text) +SECRET=$(aws ssm get-parameter --name /miti99bot/prod/telegram-webhook-secret \ + --with-decryption --query 'Parameter.Value' --output text) +TOKEN=$(aws ssm get-parameter --name /miti99bot/prod/telegram-bot-token \ + --with-decryption --query 'Parameter.Value' --output text) + +curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \ + -d "url=${URL}webhook" \ + -d "secret_token=$SECRET" \ + -d "drop_pending_updates=false" \ + -d "allowed_updates=[\"message\",\"callback_query\"]" +``` + +Verify: +```sh +curl "https://api.telegram.org/bot$TOKEN/getWebhookInfo" | jq . +``` +Expect: `url` matches Function URL, `pending_update_count` ≈ 0, `last_error_date` empty. + +## Rotate secrets + +```sh +aws ssm put-parameter --name /miti99bot/prod/telegram-webhook-secret \ + --value "$(openssl rand -hex 32)" --type SecureString --overwrite +# template.yaml uses ":1" version pin; redeploy to pick up the new value: +make sam-deploy +# Then re-run setWebhook (above) with the new secret_token. +``` + +> The `:1` in `{{resolve:ssm-secure:…:1}}` is the parameter **version** — it pins to the latest version at deploy time, not version 1 forever. To force a refresh after rotation, redeploy. + +## Rollback + +CloudFormation handles failed deploys: a failing `sam deploy` triggers automatic rollback to the prior version. To roll back a successful-but-bad deploy: + +```sh +aws cloudformation update-stack \ + --stack-name miti99bot-aws-port \ + --use-previous-template \ + --capabilities CAPABILITY_IAM +``` + +Or redeploy from a known-good commit: +```sh +git checkout +make sam-deploy +``` + +## Operational checks (daily during 7-day soak) + +```sh +# Errors / warnings in last 24h +aws logs filter-log-events --log-group-name /aws/lambda/miti99bot-aws-port-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 \ + --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 \ + --statistics Sum --start-time $(date -u -d '24 hours ago' +%FT%TZ) \ + --end-time $(date -u +%FT%TZ) --period 3600 + +# Current month spend +aws ce get-cost-and-usage --granularity MONTHLY \ + --time-period Start=$(date -u +%Y-%m-01),End=$(date -u +%F) \ + --metrics UnblendedCost +``` + +## Free-tier guardrails + +| Resource | Free | Watch when | +|---|---|---| +| Lambda req / GB-s | 1M / 400k | Past 50% mid-month | +| DynamoDB req | 200M | Past 5% (sign of runaway loop) | +| DynamoDB storage | 25 GiB | Past 100 MiB (suspect leaks) | +| EventBridge invocations | 14M | Past 1k/mo (suspect mis-config) | +| CloudWatch Logs ingest | 5 GB | Past 50% mid-month | +| Egress | 100 GB | Past 1 GB (wildly high) | + +A `$1` budget alert at 80%/100% catches all of these via cost-side fallout.