Skip to content

LastVet AWS environment (technical reference)

Status: AWS full retirement (August 2026). Production traffic: R940 (98.186.224.178) via Cox + Caddy only. Phases 0–10 complete. ryan-worker access keys deleted (2026-08-17); IAM user shell may remain until root deletes login profile + user. Residual AWS: 2 manual RDS snapshots (~$0.20/mo) + 3 KMS keys PendingDeletion (~2026-08-24). Account 525382240840 kept open, otherwise empty. Preservation: R940 /tank-bulk/aws-retirement-preservation/.
Account: 525382240840
Primary region: us-west-2 (Oregon)
IaC: Terraform in {label}

This document describes the full AWS footprint for LastVet Phase 1 API hosting: two isolated environment stacks (staging + prod), account-level bootstrap, compliance logging, billing alerts, and operational scripts.

For day-to-day console navigation, see AWS_OPS_CONSOLE.md.
For client URLs and environment pairing, see {label}.


1. Purpose and scope

LastVet runs the Node.js API on ECS Fargate with RDS PostgreSQL 18, EFS for legacy/general uploads, and (staging only) S3 + dedicated KMS for veteran imaging uploads.

On AWS: API compute, database, file storage, secrets, TLS termination at ALB, BAA-oriented audit logging.
Not on AWS: Marketing site and provider portal (Cloudflare Pages), DNS (Cloudflare, grey-cloud for API hostnames), Railway rollback net (legacy).


2. Architecture overview

flowchart TB
  subgraph clients [Clients]
    iOS[iOS app]
    Web[Provider portal / secure links]
  end

  subgraph cf [Cloudflare DNS only]
    DNS_STG[api-staging.last.vet]
    DNS_PROD[api.last.vet]
  end

  subgraph prod [Prod VPC 10.20.0.0/16]
    ALB_P[ALB + ACM]
    ECS_P[ECS Fargate api]
    RDS_P[(RDS Multi-AZ)]
    EFS_P[EFS uploads]
    LOG_P[CloudTrail / Flow / ALB logs]
  end

  subgraph stg [Staging VPC 10.10.0.0/16]
    ALB_S[ALB + ACM]
    ECS_S[ECS Fargate api]
    RDS_S[(RDS single-AZ)]
    EFS_S[EFS uploads]
    S3_S[S3 imaging + CMK]
    LOG_S[CloudTrail / Flow / ALB logs]
  end

  iOS --> DNS_STG
  iOS --> DNS_PROD
  Web --> DNS_STG
  Web --> DNS_PROD
  DNS_STG --> ALB_S
  DNS_PROD --> ALB_P
  ALB_S --> ECS_S
  ALB_P --> ECS_P
  ECS_S --> RDS_S
  ECS_P --> RDS_P
  ECS_S --> EFS_S
  ECS_P --> EFS_P
  ECS_S --> S3_S

Traffic path (API): Client → Cloudflare DNS only (grey cloud) → public ALB (HTTPS, ACM cert) → private ECS task on port 3000 → RDS / EFS / S3.

BAA note: API hostnames must stay grey-cloud. Cloudflare must not terminate TLS or proxy PHI for api.last.vet or api-staging.last.vet.


3. Environment comparison

Staging Production
Public hostname api-staging.last.vet api.last.vet
VPC CIDR 10.10.0.0/16 10.20.0.0/16
ECS cluster / service lastvet-staging-cluster / lastvet-staging-api lastvet-prod-cluster / lastvet-prod-api
RDS lastvet-staging-postgres, single-AZ lastvet-prod-postgres, Multi-AZ
RDS backup retention 14 days 35 days
RDS deletion protection off on
Desired task count 1 1
Fargate size 0.5 vCPU / 1 GiB 0.5 vCPU / 1 GiB
Container Insights disabled disabled
Boot migrations yes when RUN_MIGRATIONS_ON_BOOT=true in SM never (explicit in scripts/aws-start.sh)
Imaging S3 + CMK enabled not provisioned yet
ALB DNS lastvet-staging-alb-869788297.us-west-2.elb.amazonaws.com lastvet-prod-alb-2104741467.us-west-2.elb.amazonaws.com
ECR lastvet-staging-api lastvet-prod-api

Terraform outputs (refresh after apply):

cd infra/environments/staging && terraform output
cd infra/environments/prod && terraform output

4. Network (per environment)

Each environment is a full VPC with:

Tier Subnets Routing
Public (2 AZs) ALB, NAT gateway Internet gateway
Private (2 AZs) ECS tasks, EFS mount targets ~~NAT gateway for egress~~ no default route (Phase 3)
Database (2 AZs) RDS subnet group Same private route table (no public IP)

NAT (retired Phase 3, 2026-08-17): Both NAT gateways deleted; NAT EIPs released. Private subnets have no 0.0.0.0/0 route.

ALB + EIPs (retired Phase 5, 2026-08-17): Both ALBs, 4 listeners, 2 target groups deleted. All 4 ALB EIPs released. DNS (api.last.vet, api-staging.last.vet) points at R940 via Cloudflare, not AWS.

Not deleted (Phase 6+): VPCs, subnets, security groups, IGW, EFS, S3, CloudTrail, Secrets Manager, KMS, IAM.

Security groups (idle): alb (80/443) → ecs (3000) → rds (5432), efs (2049). RDS and ALB resources removed; SGs remain until Phase 6+.

Module: {label}


5. Compute (ECS Fargate)

Setting Value
Launch type Fargate
Platform 1.4.0
Entrypoint sh scripts/aws-start.sh → node dist/src/index.js
Health check ALB GET /health + container wget on :3000/health
Deployment min healthy 50%, max 200%
Logs CloudWatch /ecs/lastvet-{env}-api, 2192-day retention
Upload mount EFS at /data/lastvet-uploads

ECR: {account}.dkr.ecr.us-west-2.amazonaws.com/lastvet-{env}-api:latest
Deploy script: {label} (build linux/amd64, push, force new deployment).

Module: {label}


6. Database (RDS PostgreSQL 18)

Retired Phase 4 (2026-08-17): Both lastvet-staging-postgres and lastvet-prod-postgres deleted with --skip-final-snapshot --delete-automated-backups. Production database runs on R940 Postgres. Manual snapshots preserved: lastvet-staging-final-pre-retirement-2026-08-15, lastvet-prod-final-pre-retirement-2026-08-15 (available in AWS; ~$0.20/mo storage). SQL exports on R940: /tank-bulk/aws-retirement-preservation/rds-exports/.

Historical configuration (pre-retirement):

Setting Value
Engine PostgreSQL 18.4 on db.t4g.micro
Storage 20 GiB gp3, encrypted
Access Private only (no public IP)
Audit params pgaudit, log_connections=all, DDL logging
RDS logs Exported to CloudWatch (postgresql, upgrade)

Dual database URL (required)

Terraform creates two Secrets Manager entries per environment:

Secret Role ECS env var
lastvet-{env}/rds/master lastvet_master (migrations, admin) MIGRATION_DATABASE_URL
lastvet-{env}/rds/lastvet_app lastvet_app (NOBYPASSRLS) DATABASE_APP_URL

The API must use DATABASE_APP_URL at runtime so Postgres RLS policies apply. Migrations run with master URL via one-off tasks or controlled boot path (staging only).

Module: {label}


7. Storage

7.1 EFS (both environments)

Purpose General uploads (UPLOAD_DIR=/data/lastvet-uploads), ROI PDFs, legacy file paths
Encryption At rest (AWS-managed)
Access ECS task mount via access point lastvet-{env}-uploads-ap

Module: {label}

7.2 Imaging S3 (staging only)

Bucket lastvet-staging-imaging-525382240840-usw2
KMS alias alias/lastvet-staging-imaging
Encryption SSE-KMS (dedicated CMK, not audit CMK)
ECS env IMAGING_S3_BUCKET, IMAGING_KMS_KEY_ARN
CORS api-staging.last.vet, dev.provider.last.vet

Prod imaging is gated by enable_imaging_storage = false in {label} until launch.

Security detail: {label}

Module: {label}


8. Secrets Manager

Secret Contents
lastvet-{env}/app Application config (~45 keys): JWT, Stripe, SendGrid, Last1, webhooks, OAuth, etc.
lastvet-{env}/rds/master Master DB connection JSON + url
lastvet-{env}/rds/lastvet_app App role connection JSON + url

ECS task execution role reads these at task start. Terraform does not overwrite app secret values after initial placeholder (lifecycle.ignore_changes on secret string).

Sync from Railway (bootstrap / parity):

./infra/scripts/sync-railway-secrets.sh staging
./infra/scripts/sync-railway-secrets.sh prod

After editing secrets: ECS → Force new deployment.

Injected keys are listed in local.app_secret_keys in {label}.


9. DNS and TLS

Hostname Cloudflare Origin (CNAME) TLS
api-staging.last.vet DNS only (grey) staging ALB DNS name ACM cert on ALB (CN=api-staging.last.vet)
api.last.vet DNS only (grey) prod ALB DNS name ACM cert on ALB (CN=api.last.vet)

ACM certificates use DNS validation; validation CNAMEs live in Cloudflare.
HTTP listener redirects 301 → HTTPS. TLS policy: ELBSecurityPolicy-TLS13-1-2-2021-06.


10. Compliance and audit logging (BAA stack)

Per-environment module {label}:

Component Destination Retention
CloudTrail S3 lastvet-{env}-audit-logs-{account} (KMS) S3 lifecycle ~7 years
VPC Flow Logs CloudWatch /vpc/lastvet-{env}-flow-logs 2192 days
ALB access logs S3 lastvet-{env}-alb-logs-{account} (AES256) S3 lifecycle ~7 years
ECS application logs CloudWatch /ecs/lastvet-{env}-api 2192 days
RDS logs CloudWatch /aws/rds/instance/... 2192 days

KMS: Dedicated audit key alias/lastvet-{env}-audit-logs for CloudTrail and audit bucket encryption.

RDS reboot: Parameter changes for pgaudit require reboot; use {label}.


11. Account bootstrap (shared)

Applied once via {label}:

Resource Name / purpose
S3 lastvet-tfstate-525382240840 (Terraform state, versioned, encrypted)
DynamoDB lastvet-tf-locks (state locking)
SNS + alarm (us-east-1) lastvet-billing-alerts + lastvet-estimated-charges @ $200/mo estimated charges

Billing metrics publish only in us-east-1. Enable Receive CloudWatch Billing Alerts in Billing Preferences (one-time console step).

Separate from bootstrap: user-defined AWS Budget ("My Monthly Cost Budget", $100 cap) may alert earlier than the $200 CloudWatch alarm.


12. Terraform layout

infra/
├── bootstrap/                 # State bucket, locks, billing alarm (account-wide)
├── modules/
│   ├── network/               # VPC, NAT, subnets, security groups
│   ├── database/              # RDS + dual DB secrets
│   ├── storage/               # EFS uploads
│   ├── imaging-storage/       # S3 + CMK for imaging (optional)
│   ├── compute/               # ECR, ECS, ALB, ACM
│   ├── compliance-logging/    # CloudTrail, flow logs, audit S3
│   └── billing-alarms/        # us-east-1 SNS + EstimatedCharges alarm
├── stacks/api/                # Composes all modules per environment
├── environments/
│   ├── staging/               # tfvars, backend, imaging enabled
│   └── prod/
└── scripts/                   # Deploy, verify, migrate, sync secrets

Apply:

./infra/scripts/apply-environment.sh staging
./infra/scripts/apply-environment.sh prod

Default tags: Project=lastvet, Environment=staging|prod|bootstrap, ManagedBy=terraform


13. Operational scripts

Script Purpose
{label} Build, push ECR, force ECS rollout
{label} Railway → Secrets Manager parity
{label} One-off migrations via ECS task
{label} Post-Terraform DB setup (when reachable)
{label} RLS / lastvet_app verification (pass: true)
{label} Apply pending-reboot RDS parameters
{label} Load CLI creds from infra/.aws.credentials.local

Additional verify/seed scripts exist for Gate 0.5, imaging, and document RLS (see infra/scripts/run-ecs-*.sh).


14. Security and data-plane summary

Principle Implementation
No PHI at Cloudflare edge Grey-cloud DNS only for API hostnames
Two DB roles Master for migrations; lastvet_app for runtime + RLS
Secrets not in image Secrets Manager → ECS task secrets
Private data plane RDS and ECS in private subnets; no DB public access
Encryption at rest RDS, EFS, S3, KMS for audit and imaging
Audit trail CloudTrail, VPC flow, ALB access, long-retention CloudWatch
Tokens VA OAuth tokens stay on device (not on server); see platform invariants

Related: {label}, {label}


15. Cost expectations (July 2026 baseline)

Dual-environment stack with NAT ×2, Multi-AZ prod RDS, ALB ×2, and 6-year log retention typically landed ~$150–200+/month at low traffic.

Retirement savings (cumulative)

Phase Action Est. monthly savings Completed (UTC)
Phase 1 ECS scale-to-zero (staging + prod) ~$17/mo 2026-08-16 staging; 2026-08-17 prod
Phase 3 NAT Gateways + NAT EIPs removed ~$65/mo 2026-08-17T19:47:36Z
Phase 4 RDS instances deleted (staging + prod) ~$45/mo 2026-08-17T20:48:00Z
Phase 5 ALBs + listeners + TGs + 4 EIPs removed ~$32/mo 2026-08-17T20:48:00Z
Phase 6–10 Logs, EFS, S3, CloudTrail, Secrets, KMS*, IAM, VPC, ECS, ECR, DynamoDB, SNS ~$60/mo 2026-08-17T23:16:31Z
Combined ~$219/mo

*KMS keys scheduled 7-day deletion; complete ~2026-08-24.

Phase 6–10 detail: All LastVet S3 buckets, CloudTrail trails, Secrets, VPCs (staging + prod), IAM roles, EFS, CloudWatch log groups, ECS/ECR, lastvet-tf-locks, billing SNS deleted. ryan-worker retained pending final Sub-task L. Manual snapshots not deleted.

Final residual: ~$0.20/mo (2 manual RDS snapshots only, after KMS completes deletion).

Largest remaining fixed lines:

  • Manual RDS snapshots (safety net, indefinite)
  • KMS PendingDeletion (no charge after deletion completes ~2026-08-24)

Container Insights is disabled to avoid CloudWatch custom metric free-tier pressure.

Monitor: AWS Budgets (user-defined), CloudWatch lastvet-estimated-charges @ $200, Cost Explorer (console).


16. Rollback and legacy

System Role
Railway Prod/staging API rollback net; do not decommission until cutover window closes
Cloudflare Pages dev.provider.last.vet, provider.last.vet, marketing site
Rollback procedure Repoint Cloudflare CNAME to Railway; see AWS_OPS_CONSOLE.md

17. Health and verification

curl -sS https://api-staging.last.vet/health
curl -sS https://api.last.vet/health
# {"status":"ok","shield":true}

./infra/scripts/run-ecs-verify-db-role.sh staging
./infra/scripts/run-ecs-verify-db-role.sh prod
# expect "pass": true

Document Topic
AWS_OPS_CONSOLE.md Console map, bookmarks, day-to-day ops
{label} Client URLs, deploy flow, environment pairing
{label} Terraform quick start
{label} Migration rationale and parity
{label} Staging imaging bucket + KMS

Last updated: August 2026. Refresh ALB DNS names and endpoints from terraform output after infrastructure changes.