Cloud Infrastructure Architect
Design a scalable, secure, cost-optimized cloud architecture with IaC, diagrams, and a phased rollout plan.
Example run
Generated by Claude Opus 5.5 on September 23, 2026
Inputs used
- Cloud provider
- AWS
- Workload description
- We're building "VerdantConnect," a telehealth platform for Verdant Health Technologies that lets patients book appointments and have live video consultations with licensed clinicians, and lets providers manage schedules, write encounter notes, and issue e-prescriptions. Core components: - Patient-facing web app (React) and mobile apps (React Native, iOS/Android) for browsing providers, booking/rescheduling appointments, joining video visits, and viewing visit summaries. - Provider web dashboard (React) for calendar management, video consultations, clinical notes (structured encounter forms), and e-prescription submission via a third-party e-prescribing API (Surescripts-compatible). - Real-time video consultation service using WebRTC with a managed SFU (media relay) for 1:1 video/audio visits, including screen share for reviewing lab results. - Appointment scheduling and notification service (email/SMS reminders via a third-party provider) with calendar sync (Google/Outlook via OAuth). - REST/GraphQL API backend (Node.js/TypeScript microservices) handling auth, scheduling, clinical notes, billing, and notifications. - Payment processing for co-pays and self-pay visits via a PCI-compliant third-party gateway (tokenized, no raw card data stored by us). - Integration layer that syncs appointment and encounter data with partner clinics' EHR systems via HL7/FHIR APIs. - Background jobs for reminder scheduling, post-visit summary generation, and nightly reconciliation with the payment gateway. - Admin/ops console for support staff to manage user accounts, refunds, and audit logs. The system stores protected health information (PHI): patient demographics, appointment history, encounter notes, and prescription records. Video sessions are not recorded or stored. We're a Series B startup with ~40 engineers, currently at 18,000 monthly active patients and 650 active clinicians, growing roughly 12% month over month.
- Target regions
- Primary: us-east-1 (N. Virginia); DR/failover: us-west-2 (Oregon)
- Traffic and data volume
- ~18,000 MAU patients, 650 active clinicians; ~9,000 video visits/month, avg 20 min each; API traffic averages 120 req/s with peaks to 900 req/s during 8-10am and 5-7pm local business hours; Postgres primary dataset ~450 GB and growing ~25 GB/month; S3 object storage (encounter attachments, lab PDFs) ~2 TB, growing ~150 GB/month; daily HL7/FHIR sync batches of ~15,000 records with partner clinics.
- Sla or uptime goal
- 99.95% uptime for core booking and video services (max ~22 minutes downtime/month), measured monthly, with a target RTO of 30 minutes and RPO of 5 minutes for PHI data stores
- Monthly budget
- $18,000 USD/month for infrastructure (excludes third-party SaaS fees for SMS/email, e-prescribing, and payment gateway)
- Compliance requirements
- Must be HIPAA-compliant (we are a covered entity's business associate) and undergo annual SOC 2 Type II audit; targeting first SOC 2 report by Q2 2027. Specific requirements: - All PHI encrypted at rest (AES-256) and in transit (TLS 1.2+) end to end, including internal service-to-service traffic. - Signed Business Associate Agreements (BAAs) required with AWS and every third-party subprocessor (e-prescribing, SMS/email, video/SFU) that touches PHI or PII. - Detailed audit logging of all PHI access (who accessed which patient record, when, from where), retained for a minimum of 6 years per HIPAA requirements, with tamper-evident storage. - Role-based access control enforcing least privilege between patient, clinician, and admin/support roles; break-glass access procedure for emergency support cases, fully logged. - Data residency: all PHI must remain within US regions (no cross-border replication). - Automatic session timeout (15 min inactivity) and MFA required for all clinician and admin accounts. - Formal incident response plan with breach notification procedures meeting the HIPAA Breach Notification Rule (60-day notification window). - Annual third-party penetration test and quarterly vulnerability scans required for SOC 2 evidence. - Secrets (API keys, DB credentials, e-prescribing certs) must be stored in a managed secrets manager, never in code or environment files committed to source control.
- Iac tool
- Terraform
VerdantConnect: Production Architecture on AWS (HIPAA / SOC 2)
Summary. The design runs in us-east-1 as the active region with a warm standby in us-west-2. It uses:
- ECS on Fargate (Graviton) for the microservices.
- Aurora PostgreSQL Global Database for PHI.
- Amazon Chime SDK as the managed SFU. It is covered by the AWS BAA, so it adds no new subprocessor.
- Cognito for customer identity.
- An immutable, Object-Locked audit archive in a dedicated Log Archive account.
Steady-state cost is about $11.3k/month with commitments, or about $12.5k on-demand during the first 2–3 months. That leaves roughly 37% headroom under the $18k ceiling today. At 12% month-over-month growth, the ceiling is reached around months 9–10. Section 7 covers this, and it is the most important business finding in this document.
1. Assumptions and Blocking Questions
Blocking questions
I have proceeded with the defaults shown in brackets. Please confirm or correct them.
- The SLO and the regional RTO conflict. A 99.95% monthly target allows about 21.6–22.3 minutes of downtime. Your regional RTO is 30 minutes. A single regional failover therefore breaks the SLO for that month.
- Do declared regional disasters count against the SLO?
- [Default: the SLO covers AZ-level and component failures. Regional disasters are governed by the RTO/RPO and excluded from the SLO. Meeting 99.95% through a regional loss would require active-active, adding roughly $4–6k/month and substantial engineering work on data consistency.]
- Current state. Is the platform greenfield, or are the 18k MAU running live somewhere today? If live, where, and on which database?
- [Default: a live system exists on a single-region Postgres-compatible stack, and we migrate it using logical replication with a reverse-replication rollback path.]
- Workforce identity. Do you have a corporate IdP (Okta, Entra ID, or Google Workspace) for support/admin staff? Do any partner clinics want clinicians to sign in through their own IdP?
- [Default: staff federate from a corporate IdP. Clinicians use a Verdant-managed Cognito pool with passkeys or TOTP. Partner-clinic SAML/OIDC federation is added per clinic.]
Assumptions
| # | Assumption |
|---|---|
| A1 | The AWS BAA is accepted at the Organization level via AWS Artifact. PHI is processed only by HIPAA-eligible services, and this is enforced by SCPs and a service allowlist. |
| A2 | Amazon Chime SDK is acceptable as the managed SFU. The Amazon Chime end-user app was retired in Feb 2026, but the Chime SDK is a separate service and is unaffected. Confirm the roadmap with your AWS account team. A video-svc adapter keeps a switch to Daily, Vonage, or Zoom Video SDK (all offer BAAs) feasible. |
| A3 | Partner EHR integration uses FHIR R4 REST over HTTPS (OAuth2 / SMART Backend Services, optionally mTLS). Any HL7v2-over-MLLP partner needs a Site-to-Site VPN, about $36/month per connection, which is not included in the estimate. |
| A4 | Patients, clinicians, and staff are in the US. Traffic peaks follow US business hours across time zones, so the "8–10am" peak rolls from about 08:00 ET to 13:00 ET. |
| A5 | Payments use gateway-hosted fields or an iframe, which keeps PCI scope at SAQ A. No PHI (diagnosis, visit reason) is sent in payment metadata. |
| A6 | Post-visit summaries are template-based. If an LLM is introduced, use Amazon Bedrock in US regions under the AWS BAA. This is not costed. |
| A7 | Prices are approximate us-east-1/us-west-2 public list prices as of mid-2026, ±15%. Validate them in the AWS Pricing Calculator before committing. |
| A8 | The $18k budget includes non-prod and AWS Business Support. It excludes SaaS such as compliance automation (Vanta/Drata), APM, pen-test fees, and SMS/email/e-Rx/payments. |
| A9 | A platform team of 4–5 of the 40 engineers owns Terraform, CI/CD, and operations. That team size is why I chose ECS over EKS. |
| A10 | Non-production environments contain synthetic data only (no PHI). This is both a control and a cost lever. |
2. Architecture Overview
2.1 Component and data-flow diagram
Patients: Web (React) / iOS / Android Clinicians: Web (React) Support/Admin staff
│ │ │
│ static assets only (no PHI) │ │ AWS Verified Access
▼ │ │ (corp IdP + MFA + device posture)
┌─────────────────────┐ │ │
│ CloudFront + S3 │ SPA bundles, geo-restricted to US │
└─────────────────────┘ │ │
│ api.verdantconnect.com (Route 53 failover records, TTL 60s) │
▼ ▼ ▼
┌────────────────────────────── us-east-1 (ACTIVE, 3 AZs) ──────────────────────────────────┐
│ AWS WAF ──► Public ALB (TLS 1.3/1.2 policy, ACM) Internal ALB (admin) ◄── VA │
│ │ HTTPS to targets │ │
│ ┌──────────────▼───────── ECS on Fargate (Graviton), private app subnets ─────────────┐ │
│ │ api-gateway / BFF (GraphQL + REST, session validation, authZ policy engine) │ │
│ │ │ ECS Service Connect with TLS (AWS Private CA, short-lived certs) │ │
│ │ ├── identity-svc ───────── Cognito: patient pool | clinician pool │ │
│ │ ├── scheduling-svc ─────── Postgres exclusion constraints (no double-booking) │ │
│ │ ├── clinical-svc (notes, Rx) ───► e-Rx vendor (Surescripts; mTLS via NAT EIPs) │ │
│ │ ├── video-svc ──────────── Chime SDK Meetings API (control plane only) │ │
│ │ ├── billing-svc ────────── Payment gateway (tokens only, SAQ A) │ │
│ │ ├── notify-svc ─────────── SMS/email vendor (BAA; minimum-necessary content) │ │
│ │ ├── calendar-svc ───────── Google/Microsoft Graph (OAuth; events contain no PHI) │ │
│ │ ├── integration-svc ◄──── Step Functions (batched) ───► Partner EHR FHIR APIs │ │
│ │ ├── admin-svc (reachable only via internal ALB + Verified Access) │ │
│ │ └── workers (SQS consumers; Fargate + Fargate Spot): reminders, summaries, recon │ │
│ └──────────────────────────────────────────────────────────────────────────────────────┘ │
│ │TLS verify-full │TLS │TLS │ VPC endpoints (HTTPS) │
│ ┌─────▼──────────────┐ ┌──────▼──────────┐ ┌───▼───────────┐ ┌▼──────────────────────┐ │
│ │ Aurora PostgreSQL │ │ ElastiCache │ │ DynamoDB │ │ S3 PHI bucket │ │
│ │ writer + reader │ │ Valkey (cache, │ │ global table │ │ (attachments, labs) │ │
│ │ Multi-AZ, pgAudit │ │ rate limits) │ │ (sessions) │ │ SSE-KMS, versioned │ │
│ └─────┬──────────────┘ └─────────────────┘ └──────┬────────┘ └──────┬────────────────┘ │
│ │ │ │ │
│ App audit events ──► Firehose (Parquet) ──► S3 Object Lock (Log Archive acct, 7 yr) ──┐ │
│ CloudTrail (org) / VPC Flow Logs / pgAudit ─────────────────────────────────────────┘ │
└────────┼─────────────────────────────────────────────┼──────────────────┼────────────────┘
│ Aurora Global (storage-level, <1s typical) │ global table │ S3 CRR + RTC
┌────────▼──────────────── us-west-2 (WARM STANDBY) ───▼──────────────────▼────────────────┐
│ Aurora secondary cluster (1 × r7g.2xlarge, promotable) DynamoDB replica S3 replica │
│ ECS services at minimum capacity, ALB + WAF, Valkey (small), Cognito shadow pools │
│ AWS Backup vault (Vault Lock) with cross-region snapshot copies; audit archive replica │
└───────────────────────────────────────────────────────────────────────────────────────────┘
Media path (never enters our VPC):
Patient app ⇄ DTLS-SRTP ⇄ Chime SDK media (MediaRegion pinned to us-east-1 or us-west-2) ⇄ Clinician
2.2 Narrative
- Edge. Static SPA bundles are served from CloudFront. API traffic deliberately bypasses CloudFront and resolves through Route 53 failover records to a regional ALB protected by WAF.
- This keeps PHI-bearing traffic on US regional endpoints, so there are no edge-location questions for auditors.
- The trade-off is losing edge-level DDoS absorption. That is covered by Shield Standard, WAF rate-based rules, and autoscaling.
- API layer.
- A BFF/gateway service terminates user sessions. Web clients use an HttpOnly, Secure, SameSite=strict cookie; mobile clients use an opaque token.
- The gateway enforces the 15-minute idle timeout server-side and runs authorization through an in-process Cedar/OPA policy engine.
- It then calls domain services over Service Connect TLS.
- Sessions. Cognito handles authentication events only (sign-in, MFA, password reset). Our own session record lives in a DynamoDB global table, so active sessions survive a regional failover.
- Video.
video-svccreates Chime SDK meetings and attendees with pseudonymous IDs. No names, reasons for visit, or other PHI go into meeting metadata.- Clients connect directly to Chime media.
- Nothing is recorded, and media pipelines/capture are disabled by IAM policy.
- Scheduling and reminders.
- Appointments are stored in Postgres with a
tstzrangeexclusion constraint (btree_gist), so double-booking is impossible even under concurrency. - Reminders use a DB outbox table driven by a 1-minute EventBridge Scheduler tick. They do not use per-appointment schedules, which are regional resources and would complicate DR.
- Appointments are stored in Postgres with a
- Integration.
- Daily FHIR batches (~15k records) run as one Step Functions execution per partner, with a Map state over chunks of about 500 records. Per-record executions would cost about 10× more.
- Outbound traffic uses static NAT EIPs so partners and the e-Rx vendor can allowlist us.
- Audit. Every PHI read or write emits a structured audit event. Events go through Firehose to Parquet in an Object-Locked (COMPLIANCE mode) bucket in a separate Log Archive account, queryable with Athena.
3. Service Breakdown
| Layer | Service | Purpose | Why chosen |
|---|---|---|---|
| Governance | AWS Organizations + Control Tower | Accounts: Mgmt, Security Tooling, Log Archive, Backup, Shared Services, Prod, Staging, Dev, Sandbox | Blast-radius isolation, org-wide BAA acceptance, SCP guardrails. Separate audit and backup accounts make evidence tamper-resistant. |
| Edge (static) | CloudFront + S3 (OAC) | Serves React SPA bundles | Cheap and fast. No PHI, so no residency concern. |
| Edge (API) | Route 53 failover + AWS WAF + ALB | Regional API ingress, TLS 1.2+/1.3, OWASP and rate rules | Keeps PHI traffic regional. WAF managed rules and rate limits provide SOC 2 evidence. |
| Admin access | AWS Verified Access | Zero-trust access to the admin console (IdP + device posture) | Removes VPN and bastions. Every request is logged per identity. |
| Compute | ECS on Fargate (Graviton/ARM64) | All Node.js/TS microservices and workers | No node patching (less SOC 2 surface), per-task isolation, about 20% cheaper on ARM. The team is too small to justify EKS operations. |
| Service-to-service | ECS Service Connect + AWS Private CA (short-lived mode) | TLS for east-west traffic, service discovery | Meets "TLS everywhere" without a self-managed mesh. App Mesh reaches end of support on Sep 30, 2026, so it is excluded. |
| Identity (customers) | Amazon Cognito: separate patient and clinician pools | Sign-up, sign-in, MFA, passkeys | HIPAA-eligible under the AWS BAA and inexpensive. Separate pools isolate policies: clinicians have mandatory MFA; patients can use passkeys or optional MFA. |
| Identity (workforce) | IAM Identity Center + corporate IdP | AWS console/CLI and admin console SSO | Central joiner/mover/leaver process, which is core SOC 2 access-review evidence. |
| Authorization | In-process Cedar (or OPA) + Postgres RLS | RBAC plus relationship-based access (a clinician sees only their patients) | Amazon Verified Permissions at our request volume would cost roughly $1k+/month. An embedded engine costs nothing. RLS adds defense in depth. |
| Sessions | DynamoDB global table (on-demand) | Server-side sessions, 15-min idle tracking, revocation | Multi-region replication means sessions survive DR. Pennies at this scale. |
| Primary data | Aurora PostgreSQL Global Database (r7g) | Scheduling, notes, prescriptions, billing records | Multi-AZ failover in about 30–60s, cross-region lag typically under 1s (fits the 5-min RPO), managed global failover, and a global writer endpoint. |
| Cache | ElastiCache for Valkey (TLS, AUTH, CMK) | Read cache, rate limiting, idempotency keys | Non-authoritative, so no RPO is needed. Valkey is about 20% cheaper than Redis OSS. |
| Objects | S3 (SSE-KMS, versioning, CRR + RTC) + GuardDuty Malware Protection for S3 | Encounter attachments, lab PDFs | Presigned uploads, uploads scanned before release, replicated to us-west-2. |
| Video | Amazon Chime SDK Meetings | WebRTC SFU for 1:1 video/audio and screen share | Covered by the AWS BAA (no new subprocessor), US media region pinning, DTLS-SRTP, about $0.0017 per attendee-minute. |
| Async | SQS (+DLQ), EventBridge Scheduler, Step Functions | Reminders, summaries, reconciliation, FHIR batches | Serverless, HIPAA-eligible, idempotent consumers. |
| Egress control | NAT GW (static EIPs) + Route 53 Resolver DNS Firewall | Vendor allowlisting, domain-level egress allowlist | DNS Firewall provides about 80% of the exfiltration protection of Network Firewall for about 5% of the cost. See trade-offs. |
| Private AWS access | VPC interface and gateway endpoints | KMS, Secrets Manager, ECR, STS, SQS, Logs, Firehose, S3, DynamoDB | Keeps AWS API traffic off the internet and enables data-perimeter policies (aws:SourceVpce). |
| Keys | AWS KMS multi-Region CMKs (per data domain) | Encryption at rest (AES-256-GCM) and envelope encryption | Separation of duties per domain. Multi-Region keys make ciphertext usable in DR. |
| Secrets | AWS Secrets Manager (CMK, replicated, rotation) | DB credentials, API keys, e-Rx certificates | A managed secrets store as required. Rotation Lambdas. Replicas for DR. |
| Audit | Kinesis Data Firehose → S3 Object Lock (Log Archive) + Athena; CloudTrail org trail; pgAudit | PHI access trail, AWS API trail, DB trail | WORM storage (COMPLIANCE mode) plus signed digests give tamper evidence for 7 years. |
| Detection | GuardDuty, Security Hub, Config (HIPAA conformance pack), Inspector, Macie, IAM Access Analyzer, Audit Manager | Threat detection, posture, vulnerability scanning, PHI discovery, evidence | Continuous controls and evidence for SOC 2 CC6/CC7 and the HIPAA §164.308 risk management requirements. |
| Resilience | AWS Backup (Vault Lock, cross-account + cross-region), restore testing | Immutable backups against ransomware and logical corruption | Global DB replicates corruption too; locked backups do not. |
| Observability | CloudWatch (Logs data-protection masking), ADOT/X-Ray | Metrics, traces, PHI-masked logs | HIPAA-eligible and native. Masking policies catch accidental PHI in logs. |
| CI/CD | GitHub Actions (OIDC → IAM) + ECR + Terraform | Build, scan, deploy; IaC | No long-lived AWS keys. Checkov/Trivy/CodeQL gates. ECS blue/green with alarm-based rollback. |
4. Resilience and Disaster Recovery
4.1 Availability design (within the region)
- 3 AZs for ALB, ECS tasks (minimum one per AZ for critical services), NAT gateways, and endpoints.
- Aurora: writer plus a same-size reader in a different AZ (promotion tier 0), with auto-scaled readers 0–2 for peaks. Failover is typically 30–60 seconds, and clients reconnect using the cluster endpoint with retry and backoff.
- Dependency budget.
- Several dependencies publish SLAs below 99.95%; Cognito's is 99.9%.
- To reduce exposure, the BFF session design means existing users are unaffected by a Cognito brownout. Only new sign-ins are affected, and JWKS keys are cached.
- Video clients have automatic rejoin. A PSTN dial-in fallback (Chime SDK PSTN audio) can be enabled later as a degraded-mode path for visits.
- Deploy safety: ECS deployment circuit breaker plus CloudWatch-alarm rollback on 5xx rate and p95 latency. Most real-world downtime comes from deploys, not AZ failures.
4.2 Autoscaling rules
| Component | Min / Max (prod) | Scaling signal | Notes |
|---|---|---|---|
| api-gateway (BFF) | 3 / 30 tasks (1 vCPU, 2 GB) | Target tracking: ALBRequestCountPerTarget = 150; CPU 60% | Scheduled pre-warm: min 6 at 07:30–13:30 ET and 16:30–22:30 ET on weekdays. Scale-out cooldown 60s, scale-in 300s. |
| Critical domain services (identity, scheduling, video, clinical) | 3 / 20 | CPU 60%, memory 70% | Spread one per AZ. |
| Non-critical (billing, calendar, admin) | 2 / 10 | CPU 60% | |
| Workers | 1 / 20 | SQS ApproximateNumberOfMessagesVisible per task (target 100) and oldest-message age < 60s |
Capacity providers: base 1 on-demand, then 70% Fargate Spot. Consumers must be idempotent. |
| Aurora readers | 1 fixed + 0–2 auto | Reader CPU 60% or connections | Base reader doubles as the failover target. |
| Valkey | Fixed (vertical) | Memory > 70% alarm | Rebuildable cache. |
| DR region (steady state) | 1 per service (2 for gateway) | — | Raised to prod minimums by the failover runbook (SSM Automation). |
4.3 Backups
- Aurora: 35-day PITR (continuous). AWS Backup takes daily snapshots (kept 35 days) and monthly snapshots (kept 1 year; longer if your BAA or partner medical-records retention requires it). Snapshots are copied cross-account into the Backup account and cross-region to us-west-2, both with Vault Lock in compliance mode.
- S3: versioning, CRR to us-west-2, and lifecycle rules that expire noncurrent versions after 90 days (PHI bucket only; audit bucket rules are in the Terraform).
- DynamoDB: PITR enabled.
- Restore testing: AWS Backup restore-testing plans run monthly with automated validation queries. The results are SOC 2 A1.2/A1.3 evidence.
4.4 RTO/RPO by component
| Component | AZ failure (RTO / RPO) | Region failure (RTO / RPO) | How |
|---|---|---|---|
| Aurora (PHI) | ~1 min / 0 | ≤ 20 min / typically < 1 s, alarm at 60 s | Global DB managed failover (--allow-data-loss for unplanned); alarm on AuroraGlobalDBRPOLag |
| S3 attachments | 0 / 0 | ≤ 20 min / typically seconds; SLA 15 min (RTC) | CRR + RTC. See the gap noted below. |
| Sessions (DynamoDB) | 0 / 0 | 0 / ~1 s | Global table; active users stay logged in |
| Audit events | 0 / 0 | — / ≤ 60 s buffer | Firehose buffer set to 60s; the audit archive has its own CRR |
| Reminders | 0 / 0 | Same as Aurora | DB outbox, idempotent send flag |
| Video sessions | Seconds (client rejoin) | ~1–2 min after gateway failover | Clients rejoin a new meeting created via the us-west-2 Chime control plane |
| Cache | Rebuild | Rebuild | Non-authoritative |
Trade-off: S3 RPO gap. S3 Replication Time Control guarantees 99.99% of objects replicate within 15 minutes. That is outside the 5-minute RPO in the worst case, although it is typically seconds. Mitigations:
- Alarm on
ReplicationLatencyabove 300s.- A post-failover reconciliation job flags DB rows whose object is missing in the replica, and those uploads are re-requested.
- Dual-writing uploads to both regions would close the gap but doubles write complexity. I don't recommend it at this stage.
Trade-off:
rds.global_db_rpo. This Aurora PostgreSQL parameter can enforce an RPO by pausing commits when secondary lag exceeds a threshold. I do not recommend enabling it. It couples primary write availability to the health of the secondary region, so a us-west-2 problem could stop writes in us-east-1. Use alarms instead.
4.5 Regional failover runbook (target: 30 minutes)
Failover is human-declared, then automated. Automatic regional failover risks flapping and split-brain, and it would cost more than the problem it solves at this stage.
| T+ | Step | Owner / mechanism |
|---|---|---|
| 0–5 min | Detection: synthetic canaries (booking and join-visit flows) from us-west-2, Route 53 health checks, alarms → PagerDuty | On-call |
| 5 min | Incident Commander declares a regional failover. Criteria: sustained regional impairment over 10 min, or an AWS Health event confirming it. | IC |
| 5–12 min | failover-global-cluster --allow-data-loss promotes the us-west-2 Aurora cluster. The global writer endpoint follows it. |
SSM Automation document |
| 5–15 min (parallel) | Scale us-west-2 ECS to prod minimums. Enable the west reminder tick. Switch video-svc to the us-west-2 Chime control region. Promote the Cognito shadow pools. | Same SSM document |
| 15–18 min | Flip Route 53 (health-check inversion / manual failover control). Record TTL is 60s. | Runbook step |
| 18–28 min | Smoke tests (synthetic bookings, video join, note save, e-Rx sandbox ping). Run the S3 reconciliation job. | On-call + automated suite |
| ≤ 30 min | Status page updated. Covered-entity partners notified if contractually required. | IC / Comms |
Identity during regional DR (trade-off). Cognito user pools are regional and cannot export password hashes or MFA secrets.
- Existing sessions survive because they live in the DynamoDB global table.
- New sign-ins in us-west-2 go to shadow pools. These are kept in sync for profile attributes via EventBridge, but they carry no credentials.
- Patients sign in with email or SMS OTP.
- Clinicians re-verify with email OTP and then must re-enroll a passkey or TOTP before any PHI access.
- Staff are unaffected because the corporate IdP is itself multi-region.
- If this degraded path is unacceptable, the alternative is a multi-region CIAM with a BAA, such as Okta Customer Identity (Auth0) at an enterprise tier. That is SaaS spend outside this budget.
DR testing: a quarterly game day. Run the full regional failover in staging each quarter and in production twice a year during a low-traffic window, such as Sunday 02:00 ET. Record measured RTO/RPO as SOC 2 evidence.
Fargate capacity during regional events: many customers fail over at the same time. The warm minimum capacity in us-west-2 and pre-created ALB, WAF, and target groups reduce reliance on burst launches.
5. Security by Design
5.1 Account structure and IAM least privilege
- Human access goes through IAM Identity Center from the corporate IdP, with phishing-resistant MFA (FIDO2).
- There are no IAM users except two sealed break-glass users in the management account. Their hardware MFA keys are kept in separate safes, and any use of them triggers an alarm.
- Permission sets:
ReadOnly-Prodexplicitly denies the PHI data plane:s3:GetObjecton PHI buckets,secretsmanager:GetSecretValue,rds-db:connect,dynamodb:GetItemon sessions, andkms:Decrypton the PHI key.Deployis used only by CI through OIDC.PlatformAdmin-Prodis just-in-time, using AWS's open-source TEAM solution (Temporary Elevated Access Management). It requires an approver, a justification, and a session of at most 4 hours.
- Workload roles: one ECS task role per service, scoped to specific resource ARNs, with a permissions boundary on all roles that CI creates.
- Data perimeter:
- PHI bucket policies allow only named task roles via
aws:SourceVpce. - VPC endpoint policies restrict to
aws:ResourceOrgID, which blocks exfiltration to buckets in other accounts. - KMS key policies grant use only to specific roles.
- PHI bucket policies allow only named task roles via
- SCPs (org-wide):
- Deny regions other than us-east-1 and us-west-2 (data residency).
- Deny disabling CloudTrail, GuardDuty, Config, or Security Hub.
- Deny creating unencrypted RDS or EBS.
- Deny IAM user and access-key creation in workload OUs.
- Deny
organizations:LeaveOrganization. - Deny non-HIPAA-eligible services in Prod via an allowlist.
- Database:
- One Postgres role per service with schema-scoped grants.
- Row-Level Security on PHI tables using
SET LOCAL app.actor_id / app.role. - Human DB access is break-glass only: IAM DB authentication through SSM Session Manager port forwarding, fully logged.
ecs execis disabled in prod.
5.2 Network segmentation
- A VPC per environment per region with three subnet tiers:
- public: ALB and NAT only.
- private-app: ECS tasks.
- private-data: Aurora, Valkey, and interface endpoints. This tier has no route to the NAT gateway.
- Security groups reference other security groups: ALB SG → gateway SG → service SGs → data SG on specific ports only. NACLs on data subnets deny 0.0.0.0/0.
- Egress: NAT with static EIPs, plus Route 53 Resolver DNS Firewall in allowlist mode (AWS domains and named vendors: e-Rx, SMS/email, payment, Google/Microsoft calendar APIs, partner FHIR hosts).
- Trade-off: DNS Firewall does not stop connections made directly to an IP address. AWS Network Firewall does, but costs about $870/month for 3 AZs. Revisit it at SOC 2 year 2 or if pen testing shows the gap.
- VPC Flow Logs go to S3 as Parquet in the Log Archive account.
5.3 Encryption
At rest. Customer-managed KMS keys use AES-256-GCM, with separate keys per domain (phi, secrets, logs, audit), annual rotation, and multi-Region replicas only to us-west-2. Encrypted with these keys:
- Aurora and Aurora snapshots
- S3 (SSE-KMS with bucket keys, which cut KMS request costs by about 99%)
- DynamoDB, Valkey, SQS, Firehose
- CloudWatch Logs, Secrets Manager
- AWS Backup vaults
AWS Config rules detect any drift.
Field-level encryption. The AWS Encryption SDK with data-key caching protects especially sensitive fields:
- Calendar OAuth refresh tokens
- Government IDs, if collected
- Any 42 CFR Part 2 (substance-use treatment) or behavioral-health notes, if applicable
In transit:
| Hop | Control |
|---|---|
| Client → ALB | ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.3 preferred, 1.2 minimum), HSTS preload, ACM certificates |
| ALB → tasks | HTTPS target groups (task serves TLS with a Private CA–issued certificate) |
| Service → service | Service Connect TLS (Private CA, short-lived certs, automatic rotation) |
| App → Aurora | rds.force_ssl=1, ssl_min_protocol_version=TLSv1.2, client sslmode=verify-full with the RDS CA bundle |
| App → Valkey / DynamoDB / S3 / KMS | TLS; bucket policies deny aws:SecureTransport=false and s3:TlsVersion < 1.2 |
| Video media | DTLS-SRTP (Chime SDK) |
| App → vendors | TLS 1.2+, mTLS client certificate for e-Rx and FHIR where supported |
| Mobile | iOS ATS, Android Network Security Config (cleartext disabled) |
Trade-off: Service Connect TLS is one-way TLS. It encrypts traffic and authenticates the server, but it does not provide mutual TLS. Caller identity is established by a short-lived, signed internal JWT that carries the service identity and the end-user context. If auditors or pen testers push for workload-identity mTLS, VPC Lattice with IAM auth policies is the managed path, adding roughly $200–300/month.
5.4 Secrets management
- All secrets live in Secrets Manager: CMK-encrypted, replicated to us-west-2, and injected into ECS through the task definition
secrets.valueFromor fetched with the SDK and cached. Nothing is placed in.envfiles or Terraform state.- Terraform uses ephemeral resources and write-only attributes (see the Terraform starter).
- Rotation schedule:
- Service DB credentials every 30 days, using the alternating-users strategy.
- Vendor API keys every 90 days where the vendor supports it.
- The e-Rx client certificate and key are stored as a secret, with an alarm 30 days before certificate expiry.
- Leak prevention: gitleaks pre-commit hooks, GitHub secret scanning with push protection, and Checkov/Trivy in CI. A detected secret fails the build and triggers a rotation runbook.
5.5 Identity, MFA, and session timeout
| Control | Patients | Clinicians | Admin/Support |
|---|---|---|---|
| Authentication | Cognito: passkey or password + optional TOTP | Cognito: mandatory MFA, passkey (WebAuthn) preferred or TOTP; SMS not allowed | Corporate IdP (FIDO2) via Verified Access / Identity Center |
| Session idle timeout | 15 min (server-enforced) | 15 min (server-enforced) | 15 min (Verified Access / IdP + app) |
| Absolute session lifetime | 12 h | 12 h | 8 h |
| Adaptive protection | Cognito threat protection (Plus tier) on the clinician pool | Same | IdP risk policies |
- How the 15-minute timeout is enforced.
- The BFF session record stores
last_activity, written at most once per 60 seconds to control DynamoDB cost. - Any request after 15 minutes of inactivity is rejected, and the session and its refresh token are revoked.
- The client shows a 2-minute warning dialog.
- The BFF session record stores
- Edge cases: during an active video visit or note editing, heartbeat pings and keystroke-driven autosave count as activity. Drafts autosave every 30 seconds so a timeout never loses clinical notes.
- Mobile: after 15 minutes of inactivity or backgrounding, the app locks and requires biometric or PIN re-authentication. Tokens are stored in Keychain/Keystore.
FLAG_SECUREand the iOS privacy overlay protect PHI screens.
5.6 RBAC, relationship-based access, and break-glass
- Roles:
patient,clinician,clinic_admin,support_tier1,support_tier2,billing_ops,security_admin. - Policy rules (Cedar/OPA, policy-as-code in git, reviewed via PR):
- A patient may access only their own records.
- A clinician may access a patient only with an active care relationship: an upcoming or completed appointment within N days, or clinic-level assignment. RLS enforces the same rule in the database.
- Support staff see account and scheduling metadata by default and not clinical notes.
- Break-glass for support emergencies:
- The support agent requests elevated access for a specific patient with a reason code, free-text justification, and a ticket ID.
- A second approver (tier 2 or the on-call security admin) approves. For a declared P1 there is auto-approval with a mandatory review afterward.
- A time-boxed grant (60 minutes, one patient) is written to a
access_grantstable that the policy engine checks. - Every access during the grant carries
break_glass=truein the audit event, and a real-time alert goes to #security with IDs only, no PHI. - The Privacy Officer reviews all break-glass sessions weekly. Quarterly reviews are SOC 2 evidence.
- Infrastructure break-glass uses TEAM JIT roles, with sealed root and emergency IAM credentials as a last resort. Use of either triggers a PagerDuty SEV-2 page to security.
5.7 PHI access audit logging (tamper-evident, retained 6+ years)
Event schema, emitted by a shared audit library used by every service:
{ "event_id":"uuid", "ts":"RFC3339", "actor_id":"...", "actor_role":"clinician",
"actor_org":"clinic-123", "action":"READ|CREATE|UPDATE|DELETE|EXPORT|PRINT",
"resource_type":"EncounterNote", "resource_id":"...", "patient_id":"...",
"purpose":"TREATMENT|PAYMENT|OPERATIONS|BREAK_GLASS", "break_glass":false,
"source_ip":"...", "user_agent":"...", "geo":"US-VA", "session_id":"...",
"request_id":"...", "outcome":"ALLOW|DENY", "service":"clinical-svc" }
Delivery path.
- Events go to Firehose via
PutRecordBatchwith retries. If Firehose is unavailable, they fall back to an SQS queue. - If both fail, the PHI read fails closed. This trade-off favors compliance over availability, and it should be exceedingly rare.
- For writes, the audit event is written through a transactional outbox in the same DB transaction.
Storage and tamper evidence.
- Firehose converts events to Parquet, partitioned by date.
- Records land in the Log Archive account bucket with Object Lock COMPLIANCE, 7-year retention (exceeding the 6-year minimum). The bucket is replicated to us-west-2 and tiers to Glacier IR at 90 days and Deep Archive at 2 years.
- Tamper evidence beyond WORM: an hourly job writes a manifest of SHA-256 object hashes, chains it to the previous manifest's hash, and signs it with an asymmetric KMS key. CloudTrail log-file integrity validation covers the AWS API trail.
Other trails.
- pgAudit: DDL, role changes, and object-level auditing on PHI tables via
pgaudit.role, exported to CloudWatch Logs and archived. - CloudTrail org trail: includes S3 data events on PHI buckets and KMS events.
Detection rules (Athena scheduled queries and CloudWatch metric filters routed through EventBridge to PagerDuty/Slack; alerts contain IDs only):
- A clinician viewing more than N patients with no care relationship.
- Access to VIP or employee records.
- After-hours bulk reads.
- Any break-glass use.
Access to audit data. Queries of the audit data are themselves logged. Only the security_admin and Privacy Officer roles can read it. The system can produce a per-patient access report (accounting of disclosures) on request.
5.8 Data residency
- The org SCP denies every region except us-east-1 and us-west-2.
- Aurora Global, S3 CRR, DynamoDB replicas, Backup copies, KMS replicas, and Secrets replicas all target us-west-2 only.
- Chime SDK
MediaRegionis pinned server-side tous-east-1orus-west-2. Any other value is rejected. - CloudFront serves only non-PHI static assets and is geo-restricted to the US.
- Third-party residency gaps to close:
- Push notifications (APNs/FCM): no PHI in payloads. Use "You have a new message" with a deep link. Apple and Google do not sign BAAs.
- Crash reporting and analytics: no third-party trackers on authenticated screens (see the HHS OCR guidance on online tracking technologies). Use a BAA-covered, US-hosted crash tool, or scrub PHI.
- Calendar sync: events contain only "VerdantConnect visit" plus a link, with no patient name or visit reason. Personal Google/Outlook calendars are not covered by any BAA.
5.9 Vendor and BAA register (maintain it in the compliance platform)
| Subprocessor | Touches PHI/PII? | BAA / basis | Notes |
|---|---|---|---|
| AWS (all services incl. Chime SDK, Cognito) | Yes | AWS BAA via Artifact (org-level) | HIPAA-eligible services only |
| e-Prescribing vendor (Surescripts-certified) | Yes | BAA required | EPCS for controlled substances: confirm the vendor handles DEA 21 CFR 1311 identity proofing and two-factor signing |
| SMS/email provider | PII (+ appointment info) | BAA required. Alternatively, Amazon SES + AWS End User Messaging SMS falls under the AWS BAA and removes a subprocessor (about $200/month at current volume) | Reminder content follows the minimum-necessary principle |
| Payment gateway | Limited PII | HIPAA §1179 payment-processing exemption; PCI SAQ A | Send no clinical data in metadata |
| Partner clinics / EHRs | Yes | They are covered entities; agreements per the BAA chain | |
| Google/Microsoft (calendar) | Must be none | No BAA | Event content is PHI-free by design |
| Observability/APM, support desk, compliance platform | Possibly | BAA or contractual PHI prohibition plus scrubbing | Datadog, Zendesk, and others offer BAAs on specific plans |
| Pen-test firm | Should be none | NDA; test staging with synthetic data plus a scoped prod test |
5.10 Incident response and breach notification
- The plan (owned by the Security Lead, approved annually) covers:
- Severity matrix (SEV-1 to SEV-4)
- Roles: IC, Security Lead, Privacy Officer, Legal, Comms
- Runbooks for: credential compromise, PHI exposure, ransomware, vendor breach, lost device, insider misuse
- Tooling: GuardDuty, Security Hub, and Macie findings flow through EventBridge to PagerDuty.
- Forensics runbook: isolate tasks by moving them to a quarantine security group and stopping them after capturing logs; snapshot data; preserve CloudTrail and VPC Flow Logs.
- Legal hold via an Object Lock legal-hold flag.
- HIPAA breach workflow:
- Discovery starts the clock.
- The four-factor risk assessment (§164.402) is documented.
- As a business associate, you notify the covered entity without unreasonable delay and no later than 60 calendar days after discovery. Most BAAs contractually require far sooner, often 5–10 business days; track the shortest obligation per partner.
- The covered entity notifies individuals, HHS (immediately if 500+ individuals are affected), and media where more than 500 residents of a state are affected, unless it delegated this to you.
- State breach laws can be stricter (some are 30 days). Legal maintains a matrix.
- All documentation is retained for 6 years.
- Exercises: tabletop exercises twice a year (one PHI-exposure scenario, one ransomware/DR scenario). After-action reports are SOC 2 CC7.4/CC7.5 evidence.
5.11 Security testing cadence
| Activity | Cadence | Tool / owner |
|---|---|---|
| External penetration test (web, mobile, API, cloud config) | Annual, plus after major architectural change | Third-party CREST/OSCP firm. AWS permits pen testing of the services in this stack without pre-approval. |
| External vulnerability scan | Quarterly (evidence export) | Tenable or Qualys external scan |
| Container and Lambda vulnerability scanning | Continuous | Amazon Inspector (ECR enhanced scanning). Criticals fixed in 7 days, highs in 30. |
| SAST / SCA / IaC / DAST | Every PR / nightly | CodeQL or Semgrep, Dependabot, Checkov, OWASP ZAP against staging |
| Access reviews | Quarterly | IdP plus app roles, exported to the compliance platform |
| DR game day | Quarterly (staging), twice a year (prod) | Platform team |
Regulatory note: HHS proposed Security Rule amendments (NPRM, Jan 2025) that would make encryption, MFA, annual pen testing, asset inventories, and 72-hour restoration capability explicit requirements. I cannot confirm whether they have been finalized. This design already meets those proposals, so it is robust either way.
6. Security Checklist (Requirements → Controls → Evidence)
| Requirement | Concrete controls | SOC 2 / HIPAA mapping | Evidence artifact |
|---|---|---|---|
| PHI encrypted at rest (AES-256) | KMS CMKs on Aurora, S3, DynamoDB, Valkey, SQS, Logs, Backup; SCP denies unencrypted RDS; Config rules rds-storage-encrypted, s3-default-encryption-kms |
CC6.1, §164.312(a)(2)(iv) | Config conformance pack report, KMS key inventory |
| TLS 1.2+ end to end, incl. internal | ALB TLS 1.3/1.2 policy; HTTPS targets; Service Connect TLS; rds.force_ssl; bucket-policy TLS denies; Valkey in-transit encryption |
CC6.7, §164.312(e)(1) | Terraform config, SSL Labs scan, Security Hub controls |
| BAAs with AWS and all subprocessors | Artifact org BAA; vendor register with BAA status; procurement gate (no PHI integration without a signed BAA); DNS Firewall allowlist restricts egress to approved vendors only | CC9.2, §164.308(b)(1) | BAA repository, vendor risk assessments |
| PHI access audit logging, 6+ years, tamper-evident | App audit library; Firehose → Object Lock COMPLIANCE 7 yr; signed hash-chain manifests; CloudTrail validation; pgAudit; separate Log Archive account | CC7.2, §164.312(b), §164.316(b)(2) | Sample queries, Object Lock config, manifest verification job output |
| RBAC least privilege | Cognito groups → Cedar/OPA policies; care-relationship rule; Postgres RLS; per-service IAM roles; permission boundaries | CC6.1, CC6.3, §164.312(a)(1), §164.308(a)(4) | Policy repo, PR reviews, quarterly access reviews |
| Break-glass, fully logged | Two-person approval, 60-min single-patient grants, break_glass audit flag with real-time alert, weekly review; infrastructure JIT through TEAM |
CC6.2, §164.312(a)(2)(ii) | Break-glass log, weekly review sign-offs |
| US-only data residency | Region-deny SCP; replication targets limited to us-west-2; Chime MediaRegion pinning; PHI-free push and calendar content | CC6.1, contractual | SCP JSON, replication configs |
| 15-min idle timeout | BFF server-side session enforcement; mobile app lock; IdP/Verified Access session policy | CC6.1, §164.312(a)(2)(iii) | Config screenshots, automated E2E test |
| MFA for clinicians and admins | Cognito clinician pool MFA = REQUIRED (passkey/TOTP, no SMS); IdP FIDO2 for staff; Identity Center enforced | CC6.1, §164.312(d) | Cognito config export, IdP policy |
| Incident response and breach notification | IR plan; PagerDuty routing; forensics runbooks; four-factor assessment template; per-partner notification SLA matrix; tabletop exercises twice a year | CC7.3–CC7.5, §164.308(a)(6), §164.410 | Plan document, tabletop reports |
| Pen test and quarterly scans | Annual third-party test; quarterly external scans; Inspector continuous scanning; remediation SLAs tracked | CC4.1, CC7.1, §164.308(a)(8) | Reports, Jira remediation tickets |
| Managed secrets, none in code | Secrets Manager + rotation; ECS valueFrom; TF write-only/ephemeral; gitleaks + push protection |
CC6.1, CC8.1 | Rotation logs, CI scan results |
| Backups and contingency plan | AWS Backup Vault Lock, cross-account and cross-region; monthly restore tests; DR game days | A1.2, A1.3, §164.308(a)(7) | Restore-test reports, game-day RTO/RPO records |
| Change management | PR review required; Terraform plans in CI with Checkov; production deploys only via pipeline; ECS rollback on alarms | CC8.1 | GitHub audit log, pipeline records |
| Risk analysis | Annual HIPAA Security Risk Analysis plus SOC 2 risk assessment | CC3.1–CC3.4, §164.308(a)(1)(ii)(A) | SRA document |
| Workforce training | HIPAA and security training at hire and annually; phishing simulation | CC1.4, CC2.2, §164.308(a)(5) | Training completion records |
7. Terraform Starter
This is a representative slice of the data layer, audit archive, service mesh TLS, and org guardrails. VPC, IAM policy documents, and task definitions are referenced as omitted modules and files.
###############################################################################
# VerdantConnect – production starter (representative, not complete)
#
# Suggested layout (separate state per layer, blast-radius isolation):
# live/org/ -> SCPs, org-level config (management account)
# live/prod/network -> VPCs, endpoints, DNS Firewall
# live/prod/data -> KMS, Aurora Global, S3, DynamoDB (this file)
# live/prod/compute -> ECS cluster/services, ALB, WAF
# live/log-archive/ -> audit archive, CloudTrail bucket
#
# Requires Terraform >= 1.11 (ephemeral resources, write-only arguments,
# S3-native state locking) and AWS provider 6.x. Verify argument names against
# the provider version you pin before applying.
###############################################################################
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.7"
}
}
backend "s3" {
bucket = "vc-tfstate-prod-use1" # versioned, SSE-KMS, access-logged, BPA on
key = "prod/data/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "alias/vc-tfstate"
use_lockfile = true # S3-native locking; no DynamoDB lock table needed
}
}
variable "prod_account_id" { type = string }
variable "log_archive_account_id" { type = string }
variable "firehose_role_arn" { type = string } # prod-account Firehose delivery role
variable "aurora_engine_version" {
type = string
description = "Pin to an Aurora PostgreSQL 16.x version that supports Global Database."
default = "16.6"
}
locals {
tags = {
Application = "verdantconnect"
Environment = "prod"
DataClassification = "phi" # drives Config rules, Macie scope, and cost allocation
Owner = "platform"
ManagedBy = "terraform"
}
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# --- Providers -----------------------------------------------------------------
provider "aws" {
region = "us-east-1"
allowed_account_ids = [var.prod_account_id] # prevents applying to the wrong account
default_tags { tags = local.tags }
}
provider "aws" {
alias = "dr"
region = "us-west-2" # only permitted DR region (see SCP)
allowed_account_ids = [var.prod_account_id]
default_tags { tags = local.tags }
}
provider "aws" {
alias = "log_archive"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::${var.log_archive_account_id}:role/vc-terraform-deployer"
}
default_tags { tags = local.tags }
}
###############################################################################
# KMS – one CMK per data domain, multi-Region so ciphertext is portable to DR.
###############################################################################
locals {
kms_domains = toset(["phi", "secrets", "logs"])
}
resource "aws_kms_key" "this" {
for_each = local.kms_domains
description = "VerdantConnect ${each.key} CMK (prod)"
multi_region = true
enable_key_rotation = true
rotation_period_in_days = 365
deletion_window_in_days = 30
# Least-privilege key policy: named task roles only, no kms:* for humans.
policy = data.aws_iam_policy_document.kms[each.key].json # defined in kms_policies.tf
}
resource "aws_kms_alias" "this" {
for_each = local.kms_domains
name = "alias/vc-${each.key}"
target_key_id = aws_kms_key.this[each.key].key_id
}
resource "aws_kms_replica_key" "dr" {
for_each = local.kms_domains
provider = aws.dr
primary_key_arn = aws_kms_key.this[each.key].arn
deletion_window_in_days = 30
policy = data.aws_iam_policy_document.kms[each.key].json
}
###############################################################################
# Aurora PostgreSQL Global Database
# RPO: storage-level replication, typically < 1s (alarm on AuroraGlobalDBRPOLag)
# NOTE: we intentionally do NOT set rds.global_db_rpo – it would block commits
# in us-east-1 whenever us-west-2 lags, coupling primary availability to DR.
###############################################################################
locals {
aurora_params = [
{ name = "rds.force_ssl", value = "1", apply = "immediate" },
{ name = "ssl_min_protocol_version", value = "TLSv1.2", apply = "immediate" },
{ name = "shared_preload_libraries", value = "pg_stat_statements,pgaudit", apply = "pending-reboot" },
{ name = "pgaudit.log", value = "ddl,role", apply = "immediate" }, # session-level
{ name = "pgaudit.role", value = "rds_pgaudit", apply = "immediate" }, # object-level on PHI tables
{ name = "log_connections", value = "1", apply = "immediate" },
{ name = "log_disconnections", value = "1", apply = "immediate" },
]
}
resource "aws_rds_cluster_parameter_group" "primary" {
name = "vc-prod-aurora-pg16"
family = "aurora-postgresql16"
dynamic "parameter" {
for_each = local.aurora_params
content {
name = parameter.value.name
value = parameter.value.value
apply_method = parameter.value.apply
}
}
}
resource "aws_rds_cluster_parameter_group" "dr" {
provider = aws.dr
name = "vc-prod-aurora-pg16"
family = "aurora-postgresql16"
dynamic "parameter" {
for_each = local.aurora_params
content {
name = parameter.value.name
value = parameter.value.value
apply_method = parameter.value.apply
}
}
}
resource "aws_rds_global_cluster" "vc" {
global_cluster_identifier = "vc-prod-global"
engine = "aurora-postgresql"
engine_version = var.aurora_engine_version
database_name = "verdant"
storage_encrypted = true
deletion_protection = true
}
# Master password is generated ephemerally and written only to write-only
# arguments, so it NEVER lands in Terraform state or plan files.
# Services do not use this credential: each has its own rotated DB role.
ephemeral "random_password" "db_master" {
length = 40
special = true
override_special = "!#$%^&*()-_=+[]{}<>:?" # excludes / @ " and space (RDS-invalid)
}
resource "aws_secretsmanager_secret" "db_master" {
name = "prod/aurora/master"
description = "Break-glass Aurora master credential (rotated by Lambda)"
kms_key_id = aws_kms_key.this["secrets"].arn
replica {
region = "us-west-2"
kms_key_id = aws_kms_replica_key.dr["secrets"].arn
}
}
resource "aws_secretsmanager_secret_version" "db_master" {
secret_id = aws_secretsmanager_secret.db_master.id
secret_string_wo = jsonencode({
username = "vc_admin"
password = ephemeral.random_password.db_master.result
})
secret_string_wo_version = 1 # bump to push a new value from Terraform
}
# aws_secretsmanager_secret_rotation (30-day, alternating-users Lambda) omitted.
resource "aws_rds_cluster" "primary" {
cluster_identifier = "vc-prod-use1"
global_cluster_identifier = aws_rds_global_cluster.vc.id
engine = aws_rds_global_cluster.vc.engine
engine_version = aws_rds_global_cluster.vc.engine_version
master_username = "vc_admin"
master_password_wo = ephemeral.random_password.db_master.result
master_password_wo_version = 1
storage_encrypted = true
kms_key_id = aws_kms_key.this["phi"].arn
# Aurora Standard storage by default. Switch to I/O-Optimized
# (storage_type = "aurora-iopt1") once I/O exceeds ~25% of Aurora spend.
db_subnet_group_name = module.network.aurora_subnet_group_name # data tier, no NAT route
vpc_security_group_ids = [module.network.aurora_sg_id]
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.primary.name
iam_database_authentication_enabled = true # break-glass human access via SSM port-forward
backup_retention_period = 35 # PITR window (RPO for logical corruption)
preferred_backup_window = "07:00-08:00" # UTC = 03:00-04:00 ET, lowest traffic
preferred_maintenance_window = "sun:08:00-sun:09:00"
copy_tags_to_snapshot = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "vc-prod-use1-final"
enabled_cloudwatch_logs_exports = ["postgresql"] # pgAudit -> CW Logs (KMS) -> archive
}
resource "aws_rds_cluster_instance" "primary" {
count = 2 # writer + same-size reader (failover tier 0) in different AZs
identifier = "vc-prod-use1-${count.index}"
cluster_identifier = aws_rds_cluster.primary.id
instance_class = "db.r7g.2xlarge"
engine = aws_rds_cluster.primary.engine
engine_version = aws_rds_cluster.primary.engine_version
availability_zone = local.azs[count.index]
promotion_tier = 0
performance_insights_enabled = true
performance_insights_kms_key_id = aws_kms_key.this["phi"].arn
monitoring_interval = 15
monitoring_role_arn = aws_iam_role.rds_enhanced_monitoring.arn # iam.tf
auto_minor_version_upgrade = false # upgrades go through staging first (change mgmt)
}
resource "aws_rds_cluster" "secondary" {
provider = aws.dr
cluster_identifier = "vc-prod-usw2"
global_cluster_identifier = aws_rds_global_cluster.vc.id
engine = aws_rds_global_cluster.vc.engine
engine_version = aws_rds_global_cluster.vc.engine_version
storage_encrypted = true
kms_key_id = aws_kms_replica_key.dr["phi"].arn # region-local key required
db_subnet_group_name = module.network_dr.aurora_subnet_group_name
vpc_security_group_ids = [module.network_dr.aurora_sg_id]
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.dr.name
iam_database_authentication_enabled = true
deletion_protection = true
skip_final_snapshot = true
depends_on = [aws_rds_cluster_instance.primary]
lifecycle {
# Set by AWS on global-cluster membership; avoids perpetual diffs / failover drift.
ignore_changes = [replication_source_identifier, global_cluster_identifier]
}
}
resource "aws_rds_cluster_instance" "secondary" {
provider = aws.dr
identifier = "vc-prod-usw2-0"
cluster_identifier = aws_rds_cluster.secondary.id
instance_class = "db.r7g.2xlarge" # same size: no post-failover resize inside the RTO
engine = aws_rds_cluster.secondary.engine
engine_version = aws_rds_cluster.secondary.engine_version
promotion_tier = 0
}
###############################################################################
# PHI access audit archive (Log Archive account)
# WORM via S3 Object Lock COMPLIANCE mode: nobody, including root, can delete
# or shorten retention. 7 years > HIPAA's 6-year minimum.
###############################################################################
resource "aws_kms_key" "audit" {
provider = aws.log_archive
description = "PHI audit archive CMK"
enable_key_rotation = true
deletion_window_in_days = 30
policy = data.aws_iam_policy_document.audit_kms.json # grants Firehose role encrypt only
}
resource "aws_s3_bucket" "audit" {
provider = aws.log_archive
bucket = "vc-phi-access-audit-${var.log_archive_account_id}-use1"
object_lock_enabled = true
}
resource "aws_s3_bucket_ownership_controls" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
rule {
object_ownership = "BucketOwnerEnforced" # cross-account writes owned by Log Archive
}
}
resource "aws_s3_bucket_versioning" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_object_lock_configuration" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
rule {
default_retention {
mode = "COMPLIANCE"
years = 7
}
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.audit.arn
}
bucket_key_enabled = true # ~99% fewer KMS requests
}
}
resource "aws_s3_bucket_public_access_block" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
rule {
id = "tier-and-expire"
status = "Enabled"
filter {}
transition {
days = 90
storage_class = "GLACIER_IR" # still Athena-queryable, millisecond retrieval
}
transition {
days = 730
storage_class = "DEEP_ARCHIVE" # restore required before querying (12h)
}
expiration {
days = 2585 # 7 years + 30 days; Object Lock blocks any earlier delete
}
noncurrent_version_expiration {
noncurrent_days = 2585
}
}
}
data "aws_iam_policy_document" "audit_bucket" {
statement {
sid = "DenyInsecureTransport"
effect = "Deny"
actions = ["s3:*"]
resources = [aws_s3_bucket.audit.arn, "${aws_s3_bucket.audit.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
statement {
sid = "DenyTLSBelow12"
effect = "Deny"
actions = ["s3:*"]
resources = [aws_s3_bucket.audit.arn, "${aws_s3_bucket.audit.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "NumericLessThan"
variable = "s3:TlsVersion"
values = ["1.2"]
}
}
statement {
sid = "AllowFirehoseAppendOnly" # write-only: no Get/Delete for the producer
effect = "Allow"
actions = ["s3:PutObject", "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:ListBucket"]
resources = [aws_s3_bucket.audit.arn, "${aws_s3_bucket.audit.arn}/*"]
principals {
type = "AWS"
identifiers = [var.firehose_role_arn]
}
}
}
resource "aws_s3_bucket_policy" "audit" {
provider = aws.log_archive
bucket = aws_s3_bucket.audit.id
policy = data.aws_iam_policy_document.audit_bucket.json
}
# CRR of this bucket to us-west-2 (Object Lock retention replicates) omitted for brevity.
###############################################################################
# Service-to-service TLS: ECS Service Connect + AWS Private CA (short-lived mode,
# ~$50/month per CA vs ~$400 general-purpose). Root activation
# (aws_acmpca_certificate + aws_acmpca_certificate_authority_certificate) omitted.
###############################################################################
resource "aws_acmpca_certificate_authority" "svc_mesh" {
type = "ROOT"
usage_mode = "SHORT_LIVED_CERTIFICATE"
permanent_deletion_time_in_days = 30
certificate_authority_configuration {
key_algorithm = "EC_prime256v1"
signing_algorithm = "SHA256WITHECDSA"
subject {
common_name = "vc-prod-service-mesh-use1"
organization = "Verdant Health Technologies"
}
}
}
resource "aws_service_discovery_http_namespace" "core" {
name = "vc-prod.internal"
}
resource "aws_ecs_service" "api_gateway" {
name = "api-gateway"
cluster = aws_ecs_cluster.core.id # compute.tf
task_definition = aws_ecs_task_definition.api_gateway.arn # ARM64, secrets via valueFrom
desired_count = 3
launch_type = "FARGATE"
enable_execute_command = false # no interactive shells into PHI workloads
propagate_tags = "SERVICE"
network_configuration {
subnets = module.network.app_subnet_ids
security_groups = [aws_security_group.api_gateway.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.api_gateway.arn # HTTPS target group
container_name = "api-gateway"
container_port = 8443
}
deployment_circuit_breaker {
enable = true
rollback = true
}
alarms {
enable = true
rollback = true
alarm_names = [aws_cloudwatch_metric_alarm.api_5xx_rate.alarm_name]
}
service_connect_configuration {
enabled = true
namespace = aws_service_discovery_http_namespace.core.arn
service {
port_name = "https"
discovery_name = "api-gateway"
client_alias {
port = 8443
dns_name = "api-gateway.vc-prod.internal"
}
tls {
issuer_cert_authority {
aws_pca_authority_arn = aws_acmpca_certificate_authority.svc_mesh.arn
}
kms_key = aws_kms_key.this["phi"].arn
role_arn = aws_iam_role.ecs_service_connect_tls.arn # iam.tf
}
}
}
lifecycle {
ignore_changes = [desired_count] # owned by Application Auto Scaling
}
}
resource "aws_appautoscaling_target" "api_gateway" {
service_namespace = "ecs"
resource_id = "service/${aws_ecs_cluster.core.name}/${aws_ecs_service.api_gateway.name}"
scalable_dimension = "ecs:service:DesiredCount"
min_capacity = 3 # one per AZ
max_capacity = 30
}
resource "aws_appautoscaling_policy" "api_gateway_rps" {
name = "alb-requests-per-task"
policy_type = "TargetTrackingScaling"
service_namespace = aws_appautoscaling_target.api_gateway.service_namespace
resource_id = aws_appautoscaling_target.api_gateway.resource_id
scalable_dimension = aws_appautoscaling_target.api_gateway.scalable_dimension
target_tracking_scaling_policy_configuration {
target_value = 150 # req/s per 1-vCPU Node task; tune after load test
scale_out_cooldown = 60
scale_in_cooldown = 300
predefined_metric_specification {
predefined_metric_type = "ALBRequestCountPerTarget"
resource_label = "${aws_lb.api.arn_suffix}/${aws_lb_target_group.api_gateway.arn_suffix}"
}
}
}
# Pre-warm for the rolling US morning peak (08:00 ET through 10:00 PT).
resource "aws_appautoscaling_scheduled_action" "api_morning_peak" {
name = "prewarm-morning"
service_namespace = aws_appautoscaling_target.api_gateway.service_namespace
resource_id = aws_appautoscaling_target.api_gateway.resource_id
scalable_dimension = aws_appautoscaling_target.api_gateway.scalable_dimension
schedule = "cron(30 7 ? * MON-FRI *)"
timezone = "America/New_York"
scalable_target_action {
min_capacity = 6
max_capacity = 30
}
}
resource "aws_appautoscaling_scheduled_action" "api_midday_reset" {
name = "reset-midday"
service_namespace = aws_appautoscaling_target.api_gateway.service_namespace
resource_id = aws_appautoscaling_target.api_gateway.resource_id
scalable_dimension = aws_appautoscaling_target.api_gateway.scalable_dimension
schedule = "cron(30 13 ? * MON-FRI *)"
timezone = "America/New_York"
scalable_target_action {
min_capacity = 3
max_capacity = 30
}
}
# (Evening 16:30–22:30 ET pair is identical in shape.)
###############################################################################
# Org guardrails (applied from live/org with the management-account provider
# alias "aws.org"). Shown here for reference.
###############################################################################
data "aws_iam_policy_document" "scp_guardrails" {
statement {
sid = "DenyNonUSRegions" # HIPAA data-residency control
effect = "Deny"
# Global services are homed in us-east-1 (allowed), but exempt them explicitly
# so CLI/SDK calls with a different default region don't break.
not_actions = [
"iam:*", "sts:*", "organizations:*", "account:*", "route53:*",
"route53domains:*", "cloudfront:*", "support:*", "budgets:*",
"ce:*", "health:*", "trustedadvisor:*",
]
resources = ["*"]
condition {
test = "StringNotEquals"
variable = "aws:RequestedRegion"
values = ["us-east-1", "us-west-2"]
}
condition {
test = "ArnNotLike"
variable = "aws:PrincipalARN"
values = ["arn:aws:iam::*:role/AWSControlTowerExecution"]
}
}
statement {
sid = "ProtectAuditAndDetection" # tamper-resistance for SOC 2 / HIPAA audit controls
effect = "Deny"
actions = [
"cloudtrail:StopLogging", "cloudtrail:DeleteTrail", "cloudtrail:UpdateTrail",
"guardduty:DeleteDetector", "guardduty:DisassociateFromAdministratorAccount",
"config:StopConfigurationRecorder", "config:DeleteConfigurationRecorder",
"securityhub:DisableSecurityHub", "organizations:LeaveOrganization",
]
resources = ["*"]
condition {
test = "ArnNotLike"
variable = "aws:PrincipalARN"
values = ["arn:aws:iam::*:role/vc-security-automation"]
}
}
statement {
sid = "DenyUnencryptedRDS"
effect = "Deny"
actions = ["rds:CreateDBCluster", "rds:CreateDBInstance"]
resources = ["*"]
condition {
test = "Bool"
variable = "rds:StorageEncrypted"
values = ["false"]
}
}
}
# resource "aws_organizations_policy" "guardrails" {
# provider = aws.org
# name = "vc-phi-guardrails"
# type = "SERVICE_CONTROL_POLICY"
# content = data.aws_iam_policy_document.scp_guardrails.json
# }
# resource "aws_organizations_policy_attachment" "root" {
# provider = aws.org
# policy_id = aws_organizations_policy.guardrails.id
# target_id = var.org_root_id # residency applies org-wide
# }
CI guardrails for this code:
terraform planruns on every PR, and Checkov/Trivy fail the build on high-severity findings.- A Conftest/OPA policy requires
DataClassification=phiresources to set a KMS key. - Applies to prod happen only from the main branch through the GitHub OIDC deploy role, with a required reviewer.
8. Estimated Monthly Cost
Assumptions: 1-year no-upfront Reserved Instances for Aurora and Valkey, and a 1-year Compute Savings Plan for Fargate. Both are bought after 4–6 weeks of measured steady state. Prices are list, approximate (±15%). Traffic is as stated: 120 req/s average, 900 peak, 9k visits.
| # | Item | Sizing / driver | $/mo |
|---|---|---|---|
| Primary: us-east-1 | |||
| 1 | ECS Fargate (Graviton) | ~32 vCPU / 80 GB average across ~10 services + workers (peaks via scaling), Compute SP | 800 |
| 2 | Aurora PostgreSQL compute | 2 × db.r7g.2xlarge (writer + reader), 1-yr RI | 1,110 |
| 3 | Aurora storage, I/O, backups | 450 GB Standard storage, ~1.2B I/Os, 35-day PITR | 400 |
| 4 | ElastiCache Valkey | 2 × cache.m7g.large, RI | 140 |
| 5 | DynamoDB sessions (global table, both regions) | On-demand, throttled activity writes | 50 |
| 6 | ALBs (public API + internal admin) | ~10 LCU average | 90 |
| 7 | AWS WAF | 1 web ACL, ~10 rules, ~310M requests | 210 |
| 8 | CloudFront (SPA static) | Low volume | 30 |
| 9 | Amazon Chime SDK | 9,000 × 20 min × 2 attendees = 360k attendee-min × ~$0.0017 | 615 |
| 10 | S3 PHI bucket + requests + malware scanning | 2 TB Standard, +150 GB/month | 120 |
| 11 | NAT gateways (3), ~12 interface endpoints × 3 AZ, public IPv4 | 450 | |
| 12 | Data transfer out | ~2–3 TB API/PDF egress | 250 |
| 13 | SQS, EventBridge, Step Functions (batched), Lambda | 60 | |
| 14 | Route 53 (zones, health checks, DNS Firewall) | 20 | |
| Primary subtotal | 4,345 | ||
| DR: us-west-2 (warm standby) | |||
| 15 | Aurora secondary compute | 1 × db.r7g.2xlarge, RI | 560 |
| 16 | Aurora secondary storage + replicated write I/O | 150 | |
| 17 | ECS warm standby | ~9 vCPU minimum | 250 |
| 18 | Valkey (small) | 1 × cache.t4g.medium | 40 |
| 19 | ALB + WAF (low traffic) | 60 | |
| 20 | NAT (2) + endpoints (2 AZ) | 250 | |
| 21 | S3 replica (Standard-IA) + CRR + RTC | 45 | |
| 22 | AWS Backup cross-region/cross-account copies (locked) | 150 | |
| DR subtotal | 1,505 | ||
| Security, compliance, observability (all accounts) | |||
| 23 | Audit pipeline: Firehose (5 KB record rounding), Parquet conversion, Object Lock S3, Athena | ~300M events/month | 120 |
| 24 | CloudTrail org trail + PHI S3/KMS data events | 60 | |
| 25 | GuardDuty (incl. ECS runtime, RDS, S3 protection) | 300 | |
| 26 | Security Hub + AWS Config (periodic recording for ECS tasks) + Audit Manager | 330 | |
| 27 | Inspector (ECR) + Macie (targeted sampling jobs) | 200 | |
| 28 | Verified Access (admin console) | 1 app, ~20 staff | 220 |
| 29 | KMS + Secrets Manager (replicated) + Private CA (short-lived, ×2) | 220 | |
| 30 | Cognito (Essentials for patients, Plus for clinicians) | ~18.7k MAU | 150 |
| 31 | CloudWatch Logs/metrics/alarms, ADOT/X-Ray | ~500 GB logs, IA class where possible | 500 |
| Security/observability subtotal | 2,100 | ||
| Platform | |||
| 32 | Non-prod (staging at ~25% scale + dev; Aurora Serverless v2 with auto-pause in dev; synthetic data) | 2,200 | |
| 33 | Shared services (ECR, CI, TF state) | 120 | |
| 34 | AWS Business Support | ~10% of first $10k + 7% above | 1,020 |
| TOTAL (steady state, committed) | ≈ $11,290 | ||
| First 2–3 months, on-demand before commitments | ≈ $12,500 |
Unit economics: about $1.25 of infrastructure per video visit. Track this monthly as the primary FinOps KPI.
Services deliberately not included, and why:
| Service | Cost | Reason for excluding |
|---|---|---|
| Shield Advanced | $3,000/month | WAF plus Shield Standard is adequate at this threat profile |
| ARC routing-control cluster | ~$1,800/month | Manual Route 53 failover meets the 30-minute RTO |
| OpenSearch for audit | ~$600+/month | Athena on Parquet is enough |
| HealthLake | — | Not needed for pass-through FHIR sync |
| CloudTrail Lake | — | Duplicates the S3 archive |
| Network Firewall | ~$870/month | DNS Firewall as an interim control |
Growth projection (the key finding)
At 12% month-over-month growth, usage roughly doubles every 6 months (1.12⁶ ≈ 1.97) and grows about 3.9× per year. About $2.8k of today's bill scales with usage: Chime, Fargate, WAF requests, egress, logs, audit, Cognito, and Aurora I/O.
| Horizon | Usage multiple | Est. monthly infra |
|---|---|---|
| Today | 1.0× | ~$11.3k |
| +6 months (Mar 2027) | ~2.0× | ~$15k |
| +9 months (Jun 2027) | ~2.8× | ~$18k (ceiling) |
| +12 months (Sep 2027) | ~3.9× | ~$22–23k (includes an Aurora step up to r7g.4xlarge) |
Recommendation: convert the fixed $18k ceiling into a unit-cost budget, for example ≤ $1.10 infrastructure per visit, falling as fixed costs amortize. Set AWS Budgets alerts at 80% and 100% of forecast, plus anomaly detection per cost-allocation tag.
Cost-optimization levers
- Graviton everywhere (already applied), plus Fargate Spot for idempotent workers.
- Commitments laddered quarterly rather than bought as one large purchase, so sizing can follow growth.
- Aurora I/O-Optimized once I/O exceeds ~25% of Aurora spend. Cache calendar and availability reads in Valkey. Tune queries with Database Insights.
- Throttle session-activity writes to one per 60s. Aggregate audit events client-side to approach 5 KB per record, since Firehose bills in 5 KB increments.
- Put gateway endpoints (S3, DynamoDB) in front of NAT so NAT data-processing charges stay low.
- CloudWatch Logs Infrequent Access class for verbose application logs. Set retention to 30 days hot, with an S3 archive beyond that.
- At about $250k/year of spend, ask AWS about private pricing or an EDP.
Top 3 cost risks
| # | Risk | Mitigation |
|---|---|---|
| 1 | Growth breaches the fixed ceiling around month 9–10. Chime attendee-minutes, Fargate, WAF request charges, and egress all scale linearly with visits. | Re-baseline the budget to $/visit now. Ladder commitments. Review Chime pricing versus BAA-covered alternatives at ~30k visits/month (keep the video-svc adapter). Consider WAF rule tuning, or shifting static and cacheable traffic to CloudFront to cut per-request WAF charges. |
| 2 | Log, audit, and telemetry sprawl. Full-read pgAudit, debug logs, Config continuously recording short-lived Fargate tasks, Macie full-bucket scans, and Firehose 5 KB rounding can silently add $1–3k/month. | Log budgets per service, enforced with CloudWatch metric alarms. pgAudit object-level only on PHI tables. Config periodic recording for AWS::ECS::Task. Macie sampling jobs rather than full scans. Audit event aggregation. |
| 3 | Data-tier step changes. 25 GB/month of growth plus traffic growth pushes Aurora from 2xlarge to 4xlarge (about +$1.1k/month ×3 instances) and increases I/O. | Read/write splitting to readers. Aggressive caching of provider availability. Partition or archive encounter data older than 24 months to S3 (Parquet) with an on-demand restore path, if the retention policy allows. Evaluate I/O-Optimized and Database Savings Plans/RI flexibility at each renewal. |
9. Implementation Roadmap
Critical timing constraint: a first SOC 2 Type II report by Q2 2027 requires an observation period, typically at least 3 months for a first report (confirm with your auditor). That means controls must be operating by about January 4, 2027, roughly 14 weeks from today. This drives the sequencing below.
| Phase | Dates (2026–27) | Scope | Exit criteria / milestones | Depends on |
|---|---|---|---|---|
| 0. Foundations | Sep 28 – Oct 23 | Org and Control Tower accounts; SCPs (region, audit protection, encryption); org-level BAA in Artifact; Identity Center + IdP; org CloudTrail, Config (HIPAA conformance pack), Security Hub, GuardDuty, Inspector; Log Archive with Object Lock; TF state and CI (GitHub OIDC, Checkov); KMS keys; vendor BAA inventory started; compliance platform onboarded; policy set drafted (IR, access control, risk management, change management, BCP/DR, vendor management) | Security Hub baseline > 85% passing; no IAM users; SCPs active | — |
| 1. Primary platform | Oct 19 – Nov 27 | Network module (3-tier, endpoints, DNS Firewall); ECS cluster, ALB, WAF; Service Connect TLS; Aurora (single-region Multi-AZ first); Valkey; DynamoDB sessions; S3 PHI bucket and malware scanning; Cognito pools, BFF sessions, MFA, 15-min timeout; Chime integration; audit library and Firehose pipeline; Secrets Manager with rotation; staging mirrors prod | Load test at 2× peak (1,800 req/s) passes p95 < 300 ms; audit completeness test (100% of PHI endpoints emit events); internal security review | Phase 0 |
| 2. Production migration | Nov 30 – Dec 18 (before the holiday change freeze) | Logical replication from the current DB into Aurora; dual-run with Route 53 weighted shifts of 5% → 25% → 50% → 100% over about 5 business days; S3 object sync (DataSync); vendor endpoints switched to new NAT EIPs (partners allowlist in advance) | 100% of traffic on the new platform for 7 days with no SEV-1/2; old stack kept warm 30 days | Phase 1; partner allowlisting (2-week lead time) |
| 3. DR region | Dec 1 – Jan 15 (overlaps) | Aurora Global secondary; S3 CRR + RTC; DynamoDB replica; Secrets and KMS replicas; west ECS warm standby; Cognito shadow pools; locked Backup vaults; SSM failover automation; first DR game day in staging, then prod | Measured RTO ≤ 30 min and RPO ≤ 5 min in a documented game-day report | Phase 2 (ideally), or run in parallel against staging |
| 4. SOC 2 readiness and observation | Dec 14 – Apr 2 | Policies approved and training completed by Dec 31; observation window Jan 4 – Apr 2, 2027; first quarterly external scan (Jan); third-party pen test (Feb) with remediation; IR tabletop (Feb); quarterly access review (Mar); DR game day evidence; optional SOC 2 Type I in January as an interim sales artifact | Readiness assessment clean; pen-test criticals and highs closed | Phases 0–3 controls operating |
| 5. Audit and optimization | Jan – Jun 2027 | Buy RIs and Savings Plans (Jan–Feb, after steady state); right-sizing; unit-cost dashboards; auditor fieldwork (April); Type II report (May–June 2027); second quarterly scan (April); evaluate Network Firewall and mTLS (Lattice) based on pen-test findings | Report issued; cost ≤ unit target | Phase 4 |
Rollback strategy by phase
| Phase | Rollback mechanism |
|---|---|
| 0 | SCPs are attached first to a Sandbox OU for 1 week before the root; a policy detach takes effect immediately. |
| 1 | No production traffic yet; destroy and recreate via Terraform. State is versioned in S3. |
| 2 | Route 53 weighted records return to the old stack in about 60 seconds. Reverse logical replication (Aurora → old DB) runs during the dual-run so writes made on the new platform are not lost. Rollback trigger: error rate > 1% or p95 > 2× baseline for 10 minutes. The data-divergence check runs hourly. |
| 3 | The DR region is additive. Detaching the secondary from the global cluster has no primary impact. Game-day failback uses managed switchover (zero data loss). |
| All deploys | ECS blue/green with alarm-based automatic rollback. Database migrations follow expand/contract (backward-compatible first, destructive changes only after 2 releases). Feature flags for risky features. |
Key dependencies and long-lead items
- Partner allowlisting of new egress IPs and the e-Rx vendor certificate re-issuance: start in week 1.
- Vendor BAAs (SMS/email, APM, support desk): legal lead time of 2–6 weeks.
- Pen-test firm booking: book now for February.
- Auditor selection: decide by November, and agree the observation window length in writing.
10. Explicit Trade-offs (ADR Summary)
| Decision | Chosen | Alternative | Why / what you give up |
|---|---|---|---|
| Container platform | ECS Fargate | EKS | Less operational load for 4–5 platform engineers and less SOC 2 surface. You give up the Kubernetes ecosystem (service mesh, operators). Revisit beyond about 100 engineers. |
| DR posture | Warm standby | Active-active | About $1.5k/month versus $5k+ plus multi-writer complexity. A regional event consumes about 30 minutes, which exceeds one month's error budget (Q1). |
| Video SFU | Chime SDK | Daily / Vonage / Zoom SDK | Under the AWS BAA, cheapest, US region pinning. Weaker UX tooling than some competitors and long-term roadmap risk, hedged by an adapter layer. |
| Identity | Cognito + BFF sessions | Auth0/Okta CIAM | Cost and BAA simplicity. Cognito is single-region, so DR sign-in is degraded (existing sessions survive). |
| API ingress | Regional ALB (no CloudFront) | CloudFront in front of the API | Clean residency story. Less edge DDoS absorption. |
| East-west security | Service Connect one-way TLS + service JWTs | mTLS via VPC Lattice | Simpler and cheaper; meets the "encrypted in transit" requirement. Mutual workload authentication is at the application layer rather than the network layer. |
| Egress control | DNS Firewall | Network Firewall | About $20/month versus about $870/month. Does not block connections made directly to IP addresses. |
| Authorization | Embedded Cedar/OPA | Amazon Verified Permissions | Avoids about $1k+/month in per-request fees. You own policy distribution and versioning. |
| Audit write failure | Fail closed on PHI reads | Fail open | Compliance integrity over availability. With the Firehose-plus-SQS fallback, this should be vanishingly rare. |
| S3 RPO | CRR + RTC (15-min SLA) | Application dual-write | Simplicity. Worst case slightly exceeds the 5-minute RPO; mitigated by reconciliation. |
Suggested next steps:
- Answer the three blocking questions, especially the SLO scope for regional disasters.
- Book the pen test and the auditor this month.
- Start Phase 0 on Monday.
I can follow up with a detailed failover SSM Automation runbook, the audit-event library interface (TypeScript), or a fuller Terraform module layout.
Related prompts
Cybersecurity Audit Specialist
Run a structured cybersecurity audit of an organization, prioritizing risks and producing an actionable remediation roadmap.
DevOps Automation Specialist
Acts as a DevOps engineer to design, optimize, and troubleshoot CI/CD pipelines, infrastructure as code, and cloud automation.
Docker Container Builder
Generates production-ready, optimized Dockerfiles with multi-stage builds, caching, and tagging for any application.
Ansible Automation Playbook Creator
Generates production-ready, idempotent Ansible playbooks and roles for any infrastructure automation or configuration task.