Skip to content

Historical only (August 2026). LastVet production and staging API traffic run on the R940 operator-controlled platform, not AWS ECS/RDS. For current topology, env lanes, and deploy gates, see lastvet-gates/docs/deployment/R940_ENVIRONMENT_TOPOLOGY.md. This file is preserved for AWS decommission audit and Terraform reference.

LastVet AWS environment (technical reference)

Status: Live (July 2026)
Account: 525382240840
Primary region: us-west-2 (Oregon)
IaC: Terraform in infra/

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 DEV_STAGING.md.


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
Database (2 AZs) RDS subnet group Same private route table (no public IP)

NAT: One NAT gateway per VPC (largest fixed cost driver).
Security groups: alb (80/443 from internet) → ecs (3000 from ALB) → rds (5432 from ECS), efs (2049 from ECS).

Module: infra/modules/network/


5. Compute (ECS Fargate)

Setting Value
Launch type Fargate
Platform 1.4.0
Entrypoint sh scripts/aws-start.shnode 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: infra/scripts/deploy-api-image.sh (build linux/amd64, push, force new deployment).

Module: infra/modules/compute/


6. Database (RDS PostgreSQL 18)

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: infra/modules/database/


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: infra/modules/storage/

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 infra/environments/prod/main.tf until launch.

Security detail: SECURITY_POSTURE_IMAGING_STORAGE.md

Module: infra/modules/imaging-storage/


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 infra/modules/compute/main.tf.


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 infra/modules/compliance-logging/:

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 infra/scripts/reboot-rds.sh.


11. Account bootstrap (shared)

Applied once via infra/bootstrap/:

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
deploy-api-image.sh Build, push ECR, force ECS rollout
sync-railway-secrets.sh Railway → Secrets Manager parity
run-ecs-db-setup.sh One-off migrations via ECS task
post-apply-db.sh Post-Terraform DB setup (when reachable)
run-ecs-verify-db-role.sh RLS / lastvet_app verification (pass: true)
reboot-rds.sh Apply pending-reboot RDS parameters
load-aws-env.sh 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: SECURITY_POSTURE_DATABASE_ACCESS.md, TRACK3_PHI_RLS_PLAN.md


15. Cost expectations (July 2026 baseline)

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

  • NAT Gateway (×2 VPCs)
  • RDS (prod Multi-AZ + staging)
  • Application Load Balancers (×2)
  • CloudWatch / S3 log storage (grows over time)

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
DEV_STAGING.md Client URLs, deploy flow, environment pairing
infra/README.md Terraform quick start
AWS_MIGRATION_ASSESSMENT.md Migration rationale and parity
SECURITY_POSTURE_IMAGING_STORAGE.md Staging imaging bucket + KMS

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