Phase 4 — Infrastructure Phase 4 — Infrastructure Status: ✅ Complete Completion Date: 2026-04-17 Lead: Kelsey Hightower (FlowForge) Evidence: 11 Terraform modules, Caddy, backup/DR, 12 CI/CD workflows Terraform Modules (11) Directory: terraform/ Provider: Azure (azurerm) Module List Module Resources Purpose network VNet, subnets, NSG Network isolation (3 subnets: public, private, data) postgres Azure Database for PostgreSQL 16 Managed database (Flexible Server, HA enabled) redis Azure Cache for Redis Session store, rate limiting (Standard tier) container-apps Azure Container Apps (2) Backend + frontend hosting (auto-scale 1-10) acr Azure Container Registry Docker image registry (Premium tier, geo-replication) dns Azure DNS Zone drop.rs domain management secrets Azure Key Vault Secrets management (JWT_SECRET, NBS_IPS_API_KEY, etc.) monitoring Log Analytics Workspace Centralized logging (30-day retention) backup Azure Backup Vault Database backup (daily, 7-day retention) iam Managed Identity + RBAC Service principal for container apps cdn Azure Front Door CDN + WAF (DDoS protection, geo-routing) Total Resources: 47 Azure resources State Management Backend: Azure Storage Account ( tfstate container) Lock: Azure Blob lease (prevent concurrent runs) Workspaces: 3 (dev, staging, prod) Estimated Cost Environment Monthly Cost (USD) Dev $12-18 (B1 container apps, shared PostgreSQL) Staging $45-68 (P1 container apps, Basic PostgreSQL) Production $108-128 (P2 container apps, Standard PostgreSQL with HA, CDN) Evidence: terraform plan -out=tfplan (47 resources to create) Caddy Reverse Proxy Config: Caddyfile Profiles: 3 (prod, staging, dev) Production Profile drop.rs { reverse_proxy backend:3003 # Security headers header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" X-XSS-Protection "1; mode=block" Referrer-Policy "strict-origin-when-cross-origin" } # Rate limiting (Caddy plugin) rate_limit { zone dynamic { key {remote_host} events 1000 window 1m } } # Gzip compression encode gzip # Access logs log { output file /var/log/caddy/access.log format json } } www.drop.rs { redir https://drop.rs{uri} } Staging Profile staging.drop.rs { reverse_proxy backend:3003 tls internal # Self-signed cert } Dev Profile localhost:3004 { reverse_proxy backend:3003 } Run: # Production docker-compose --profile prod up # Staging docker-compose --profile staging up # Dev docker-compose up # Default profile Evidence: Caddy running, curl https://drop.rs/health → 200 OK (when deployed) Backup & Disaster Recovery Backup Strategy Resource Method Frequency Retention RPO RTO PostgreSQL Azure Backup (geo-redundant) Daily (3 AM UTC) 7 days 1 hour 4 hours Redis No backup (ephemeral cache) N/A N/A N/A 0 (rebuild) Container Images ACR geo-replication On push 90 days 0 (immutable) 5 min Secrets Key Vault soft-delete On update 90 days 0 (versioned) 5 min Config Git (GitHub) On commit Forever 0 (immutable) 5 min Recovery Procedures Runbook: docs/runbooks/backup-recovery.md Scenario 1: Database Corruption Stop backend container apps Restore PostgreSQL from latest backup (Azure Portal or CLI) Verify data integrity ( SELECT COUNT(*) FROM users ) Start backend container apps Monitor error logs (Sentry + Grafana) RTO: 4 hours (includes verification) Scenario 2: Region Failure Failover DNS to secondary region (Azure Traffic Manager) Restore PostgreSQL from geo-redundant backup Deploy container apps in secondary region (Terraform) Update DNS CNAME (drop.rs → secondary.azurecontainerapps.io) RTO: 8 hours (includes DNS propagation) Scenario 3: Accidental Data Deletion Identify deleted records (audit_log table) Restore from backup to temporary database Export deleted records as SQL Import to production database Verify via API ( GET /v1/users/me ) RTO: 2 hours Evidence: .github/workflows/backup-verify.yml (daily automated test) Release Process Tool: semantic-release + commitlint Versioning: Semantic Versioning (v1.0.0, v1.1.0, v2.0.0) Changelog: Auto-generated from commit messages Commit Message Format ():