diff --git a/.github/workflows/aws-probe.yml b/.github/workflows/aws-probe.yml deleted file mode 100644 index d50183c..0000000 --- a/.github/workflows/aws-probe.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: AWS Probe (read-only) - -# Manual-only, read-only snapshot of what exists in the target AWS account. -# Uses the same OIDC role as the CD build job. Nothing here mutates anything. - -on: - workflow_dispatch: - -permissions: - contents: read - id-token: write - -jobs: - probe: - name: Read account state - runs-on: ubuntu-latest - environment: build - steps: - - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/webdev-deploy-build - aws-region: ap-southeast-1 - - name: Identity + inventory - run: | - echo '--- caller:' - aws sts get-caller-identity --query Account --output text || true - echo '--- ECR repositories:' - aws ecr describe-repositories --query 'repositoryNames' --output text || true - echo '--- CFN stacks (ap-southeast-1):' - aws cloudformation list-stacks \ - --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE CREATE_IN_PROGRESS ROLLBACK_IN_PROGRESS UPDATE_IN_PROGRESS \ - --query 'StackSummaries[?starts_with(StackId,`arn:aws:cloudformation:ap-southeast-1`) || not_contains(StackId, `us-east-1`)].StackName' \ - --output text || true - echo '--- CFN stacks (us-east-1):' - aws cloudformation list-stacks --region us-east-1 \ - --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \ - --query 'StackSummaries[].StackName' --output text || true - echo '--- SSM /webdev params:' - aws ssm describe-parameters --parameter-filters 'Key=Name,Option=BeginsWith,Values=/webdev' --query 'ParameterConfigurations[].ParameterName' --output text || true - echo '--- ECS clusters:' - aws ecs list-clusters --query 'clusterArns' --output text || true diff --git a/scripts/aws/first-deploy.sh b/scripts/aws/first-deploy.sh deleted file mode 100755 index dd4ba17..0000000 --- a/scripts/aws/first-deploy.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash -# One-time real-AWS bootstrap after scripts/aws/one-time-aws-admin.sh provided -# the [profile bootstrap] keys. Idempotent; safe to re-run after failures. -# Agent drives this; it STOPS before cutting a release tag (human call). -set -euo pipefail -cd "$(dirname "$0")/../../infra" - -ACCOUNT=772889137569 -DOMAIN=webdevstudio.resonance.io.vn -export AWS_PROFILE=bootstrap -MAIN_REGION=ap-southeast-1 -CF_REGION=us-east-1 - -echo "==> 0. identity" -WHOAMI=$(aws sts get-caller-identity --query Account --output text) -[[ "$WHOAMI" == "$ACCOUNT" ]] || { echo "wrong account: $WHOAMI (expected $ACCOUNT)"; exit 1; } - -cdk_cmd() { bunx --bun cdk "$@"; } - -echo "==> 1. bootstrap CDK toolchains (both regions)" -cdk_cmd bootstrap "aws://$ACCOUNT/$MAIN_REGION" --tags owner=webdev -cdk_cmd bootstrap "aws://$ACCOUNT/$CF_REGION" --tags owner=webdev - -echo "==> 2. network + GitHub OIDC roles" -cdk_cmd deploy webdev-network --require-approval never -cdk_cmd deploy webdev-oidc --require-approval never - -echo "==> 3. ACM certificate (CloudFront needs it in $CF_REGION)" -CERT_ARN=$(aws acm list-certificates --region "$CF_REGION" \ - --certificate-statuses ISSUED \ - --query "CertificateSummaryList[?DomainName=='$DOMAIN'].CertificateArn | [0]" --output text) -if [[ -z "$CERT_ARN" || "$CERT_ARN" == "None" ]]; then - PENDING=$(aws acm list-certificates --region "$CF_REGION" \ - --certificate-statuses PENDING_VALIDATION \ - --query "CertificateSummaryList[?DomainName=='$DOMAIN'].CertificateArn | [0]" --output text) - if [[ -z "$PENDING" || "$PENDING" == "None" ]]; then - CERT_ARN=$(aws acm request-certificate --region "$CF_REGION" \ - --domain-name "$DOMAIN" --validation-method DNS --key-algorithm rsa-2048 \ - --query CertificateArn --output text) - else - CERT_ARN="$PENDING" - fi - ZONE_ID=$(aws route53 list-hosted-zones --query "HostedZones[?Name=='$DOMAIN.'].Id | [0]" --output text | sed 's|/hostedzone/||') - [[ "$ZONE_ID" != "None" ]] || { echo "no Route53 zone for $DOMAIN — did webdev-network create it?"; exit 1; } - RES_NAME=$(aws acm describe-certificate --region "$CF_REGION" --certificate-arn "$CERT_ARN" \ - --query 'DomainValidationOptions[0].ResourceRecord.Name' --output text) - RES_TYPE=$(aws acm describe-certificate --region "$CF_REGION" --certificate-arn "$CERT_ARN" \ - --query 'DomainValidationOptions[0].ResourceRecord.Type' --output text) - RES_VAL=$(aws acm describe-certificate --region "$CF_REGION" --certificate-arn "$CERT_ARN" \ - --query 'DomainValidationOptions[0].ResourceRecord.Value' --output text) - aws route53 change-batch --hosted-zone-id "$ZONE_ID" --wait \ - --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"$RES_NAME\",\"Type\":\"$RES_TYPE\",\"TTL\":60,\"ResourceRecords\":[{\"Value\":\"$RES_VAL\"}]}}]}" >/dev/null - echo " waiting for DNS validation…" - for i in $(seq 1 30); do - ST=$(aws acm describe-certificate --region "$CF_REGION" --certificate-arn "$CERT_ARN" --query Certificate.Status --output text) - [[ "$ST" == "ISSUED" ]] && break - sleep 10 - done - [[ "$ST" == "ISSUED" ]] || { echo "cert still $ST — re-run later"; exit 1; } -fi -echo " cert: $CERT_ARN" - -echo "==> 4. WAF (CloudFront scope) + SSM pointers the prod stack reads" -cdk_cmd deploy webdev-waf --require-approval never -WAF_ARN=$(aws wafv2 list-web-acls --region "$CF_REGION" --scope CLOUDFRONT \ - --query "WebACLs[?starts_with(Name, 'webdev')].ARN | [0]" --output text) -[[ "$WAF_ARN" != "None" ]] || { echo "webdev WAF ACL not found"; exit 1; } - -echo "==> 5. SSM parameters (prod stack inputs)" -for pair in "/webdev/cloudfront/cert-arn=$CERT_ARN" "/webdev/cloudfront/waf-arn=$WAF_ARN"; do - aws ssm put-parameter --region "$MAIN_REGION" --name "${pair%%=*}" --value "${pair#*=}" --type String --overwrite >/dev/null - echo " ${pair%%=*} = ${pair#*=}" -done - -echo "==> 6. probe sanity (from CI side):" -echo " OIDC provider: $(aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[?contains(Arn,`github`)].Arn | [0]' --output text)" -echo -echo "Bootstrap complete. Next (human decision — real money):" -echo " gh workflow run aws-probe.yml # should go green now" -echo " git tag v0.1.1 && git push origin v0.1.1 && gh release create v0.1.1 ..." diff --git a/scripts/aws/one-time-aws-admin.sh b/scripts/aws/one-time-aws-admin.sh deleted file mode 100755 index f4ef83a..0000000 --- a/scripts/aws/one-time-aws-admin.sh +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env bash -# -# A wizard walks a human through a manual procedure, step by step. -# Generated by the /wizard skill. -# -# Everything above the "STAGES" marker is the wizard library: do not hand-edit -# it. Author the per-step stages below the marker. - -set -euo pipefail - -# ────────────────────────────────────────────────────────────────────────── -# Wizard library: delightful, consistent UX, identical across every wizard. -# ────────────────────────────────────────────────────────────────────────── - -if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then - BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0) - BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1) -else - BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED="" -fi - -# Author sets this at the top of the stages section. -TOTAL_STAGES=0 - -_STAGE_INDEX=0 -ENV_FILE="${ENV_FILE:-.env}" -WRITTEN_ENV=() # KEYs written to ENV_FILE this run -WRITTEN_SECRET=() # secret NAMEs set this run -SKIPPED=() # things we couldn't do (e.g. gh missing) - -# _clear wipes the terminal so only the current step is on screen. No-op when -# output isn't a terminal, so piped logs stay readable. -_clear() { - [[ -t 1 ]] || return 0 - if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi -} - -# banner "Title" shows the opening frame: what this wizard does. -banner() { - _clear - printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET" - printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET" - printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM" - printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n' - printf ' later, since it remembers values already saved.%s\n' "$RESET" - pause "Ready to start?" -} - -# stage "Name" clears the screen, then announces a stage and shows progress. -# Clearing keeps only the current step on screen. -stage() { - _clear - _STAGE_INDEX=$((_STAGE_INDEX + 1)) - printf '\n%s%s▸ Stage %s/%s · %s%s\n' \ - "$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET" -} - -# say "..." prints a plain instruction line. -say() { printf ' %s\n' "$1"; } -# step "..." is a numbered-feeling action the human takes in the browser. -step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; } -note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; } -warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; } - -# open_url URL opens it in the human's browser, cross-platform incl. WSL. -open_url() { - local url="$1" - printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url" - { if command -v wslview >/dev/null 2>&1; then wslview "$url" - elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url" - elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" - elif command -v open >/dev/null 2>&1; then open "$url" - else warn "couldn't open a browser; visit it manually: $url"; fi - } >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url" -} - -# pause "msg" waits for the human to confirm they've done the manual part. -pause() { - printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET" - read -r _ || true -} - -# confirm "question" is a y/N gate; returns success on yes. -confirm() { - local reply="" - printf ' %s? %s [y/N] ' "$YELLOW" "$1" - read -r reply || true - [[ "$reply" =~ ^[Yy] ]] -} - -# _existing KEY: current value of KEY in ENV_FILE, if any. -_existing() { - [[ -f "$ENV_FILE" ]] || return 1 - local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1 - printf '%s' "${line#*=}" -} - -# ask KEY "Prompt" reads a value into $KEY. Offers the existing .env value as -# a default on re-runs (Enter keeps it). Visible input (non-secret). -ask() { - local key="$1" prompt="$2" current input - current=$(_existing "$key" || true) - if [[ -n "$current" ]]; then - printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET" - else - printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET" - fi - read -r input || true - [[ -z "$input" && -n "$current" ]] && input="$current" - printf -v "$key" '%s' "$input" -} - -# ask_secret KEY "Prompt" is like ask, but input is hidden. -ask_secret() { - local key="$1" prompt="$2" current input - current=$(_existing "$key" || true) - if [[ -n "$current" ]]; then - printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET" - else - printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET" - fi - read -rs input || true - printf '\n' - [[ -z "$input" && -n "$current" ]] && input="$current" - printf -v "$key" '%s' "$input" -} - -# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces -# any existing line). Idempotent. -write_env() { - local key="$1" value="$2" tmp - touch "$ENV_FILE" - tmp=$(mktemp) - grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true - printf '%s=%s\n' "$key" "$value" >> "$tmp" - mv "$tmp" "$ENV_FILE" - WRITTEN_ENV+=("$key") - printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE" -} - -# set_secret NAME VALUE sets a GitHub Actions repo secret via gh. Falls back -# to a warning (and records it) if gh is unavailable or unauthenticated. -set_secret() { - local name="$1" value="$2" - if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then - if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then - WRITTEN_SECRET+=("$name") - printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name" - return - fi - fi - SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)") - warn "skipped GitHub secret $name: gh not ready; set it later" -} - -# set_var NAME VALUE sets a GitHub Actions repo variable (non-secret). -set_var() { - local name="$1" value="$2" - if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then - if gh variable set "$name" --body "$value" >/dev/null 2>&1; then - printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name" - return - fi - fi - SKIPPED+=("GitHub variable $name") - warn "skipped GitHub variable $name, gh not ready; set it later" -} - -# finish clears, then shows a closing summary of everything configured. -finish() { - _clear - printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET" - (( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}" - (( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}" - if (( ${#SKIPPED[@]} )); then - printf '\n'; warn "still to do by hand:" - for s in "${SKIPPED[@]}"; do note " - $s"; done - fi - printf '\n' -} - -# ────────────────────────────────────────────────────────────────────────── -# STAGES: author this section. One stage() per step the human takes. -# Replace the example below. Set TOTAL_STAGES to match the stages you write. -# ────────────────────────────────────────────────────────────────────────── - -TOTAL_STAGES=4 - -banner "One-time AWS bootstrap access" - -EXPECTED_ACCOUNT=772889137569 - -# ── Stage 1: create the throwaway admin user ────────────────────────────── -stage "AWS Console: throwaway admin user" -say "We create a TEMPORARY admin user, capture its keys, and delete the user" -say "once the GitHub OIDC roles exist. You will never need it again." -open_url "https://console.aws.amazon.com/iam/v2/users" -step "Sign in to the AWS Console as an administrator." -step "Users → Create user → name: temp-bootstrap" -step "Permissions: 'Attach policies directly' → check AdministratorAccess → Next → Create user" -step "Open the new user → Credentials tab (or 'AWS security credentials') → Create access key" -step "Use case: CLI / 'Command Line Interface' → create, then copy both values." -ask AWS_ACCESS_KEY_ID "Paste the Access key ID (starts AKIA):" -ask_secret AWS_SECRET_ACCESS_KEY "Paste the Secret access key:" -# ── Stage 2: save as aws profile + verify identity ──────────────────────── -stage "Save profile 'bootstrap' and verify the account" -CRED_FILE="$HOME/.aws/credentials" -mkdir -p "$HOME/.aws" -touch "$CRED_FILE" -if grep -q '^\[profile bootstrap\]' "$CRED_FILE"; then - sed -i.bak '/^\[profile bootstrap\]/,+3d' "$CRED_FILE" -fi -printf '[profile bootstrap]\naws_access_key_id = %s\naws_secret_access_key = %s\nregion = ap-southeast-1\n' \ - "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" >> "$CRED_FILE" -chmod 600 "$CRED_FILE" -printf ' ✓ wrote %s[profile bootstrap]%s → %s\n' "$GREEN" "$RESET" "$CRED_FILE" -step "Verifying identity against account $EXPECTED_ACCOUNT ..." -WHOAMI=$(AWS_PROFILE=bootstrap aws sts get-caller-identity --query Account --output text 2>&1) || true -if [[ "$WHOAMI" == "$EXPECTED_ACCOUNT" ]]; then - printf ' %s✓%s keys valid for account %s (%s)\n' "$GREEN" "$RESET" "$EXPECTED_ACCOUNT" "$(AWS_PROFILE=bootstrap aws sts get-caller-identity --query Arn --output text)" -else - warn "identity check failed or wrong account: $WHOAMI" - warn "the keys must belong to AWS account $EXPECTED_ACCOUNT" -fi -# ── Stage 3: budget alarm email (cd.yml: secrets.ALERT_EMAIL) ───────────── -stage "Budget alarm email" -say "The prod CD wires an AWS budget alarm to this address (CDK only creates" -say "the alarm when the email is provided)." -ask ALERT_EMAIL "Alarm email (e.g. aws-billing@yourdomain):" -set_secret ALERT_EMAIL "$ALERT_EMAIL" -# ── Stage 4: cleanup promise ────────────────────────────────────────────── -stage "Cleanup reminder" -say "After the bootstrap finishes (OIDC provider + webdev-deploy-* roles exist" -say "and the first real release deploys), DELETE the temp user:" -say " Console → IAM → Users → temp-bootstrap → Delete user (keys first)." -say "Ask the agent to verify no webdev-deploy role trusts temp-bootstrap before" -say "deleting. Nothing else uses these keys." -if confirm "Type yes to acknowledge (deletion happens later, not now):"; then - say "Acknowledged. See stage 3 notes when you're done." -fi - -finish diff --git a/scripts/localstack/ecr-rehearsal-push.sh b/scripts/localstack/ecr-rehearsal-push.sh index b217d8e..4ca00af 100755 --- a/scripts/localstack/ecr-rehearsal-push.sh +++ b/scripts/localstack/ecr-rehearsal-push.sh @@ -5,7 +5,9 @@ set -euo pipefail cd "$(dirname "$0")/../.." -REG=000000000000.dkr.ecr.us-east-1.localhost.localstack.cloud:4566 +# Region must match what CDK synthesizes into the task definitions (ap-southeast-1). +AWS_DEFAULT_REGION=us-east-1 +REG="000000000000.dkr.ecr.$AWS_DEFAULT_REGION.localhost.localstack.cloud:4566" PAIRS="api:webdev-prod-api web:webdev-prod-web api-go:webdev-lab-api-go api-axum:webdev-lab-api-axum api-elysia:webdev-lab-api-elysia" docker exec localstack-aws awslocal ecr get-login-password | docker login --username AWS --password-stdin "$REG" >/dev/null @@ -13,8 +15,8 @@ docker exec localstack-aws awslocal ecr get-login-password | docker login --user for pair in $PAIRS; do app=${pair%%:*} repo=${pair##*:} - docker exec localstack-aws awslocal ecr describe-repositories --repository-names "$repo" >/dev/null 2>&1 || - docker exec localstack-aws awslocal ecr create-repository --repository-name "$repo" >/dev/null + docker exec localstack-aws awslocal ecr describe-repositories --region "$AWS_DEFAULT_REGION" --repository-names "$repo" >/dev/null 2>&1 || + docker exec localstack-aws awslocal ecr create-repository --region "$AWS_DEFAULT_REGION" --repository-name "$repo" >/dev/null img="wds-$app-rehearsal" docker image inspect "$img" >/dev/null 2>&1 || docker build -q -f "apps/$app/Dockerfile" -t "$img" . >/dev/null docker tag "$img" "$REG/$repo:rehearsal" @@ -23,6 +25,6 @@ for pair in $PAIRS; do done for pair in $PAIRS; do - docker exec localstack-aws awslocal ecr describe-images --repository-name "${pair##*:}" \ + docker exec localstack-aws awslocal ecr describe-images --region "$AWS_DEFAULT_REGION" --repository-name "${pair##*:}" \ --query 'imageDetails[0].imageTags[0]' --output text | xargs echo "verified ${pair##*:}:" done