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):
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.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: 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):
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:
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
18. Related documents¶
| 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.