Azure Well-Architected Assessment¶
Status: Draft Workload: hybridcloudworks.com Target maturity: Production-ready foundation with documented cost-driven exceptions Evidence read: 2026-10-15 (PLAT-9, #969)
This assessment began as an architecture baseline, not evidence of deployed controls. A control
moves from Planned to Verified only after Terraform validation, deployment, and the listed
production signal are captured. Since 2026-10-15 the Status column carries what the repository and
the live estate show, read on the date in the row: Verified <date> is the listed signal captured;
Implemented is the control in code or configuration with its signal not yet captured, the missing
measurement named in the row; Partial is part of the target built and the gap named; by decision
marks a gap the owner chose (the decision record is cited). No row is Planned any more: every
control has at least its first half built, and the Verification signal cell says where.
Executive scorecard¶
| Pillar | Current prototype | Target foundation | Principal constraint |
|---|---|---|---|
| Reliability | High risk | Production-ready single-region | USD 150 ceiling; Cosmos Serverless is single-region |
| Security | High risk | Strong identity and secret baseline | Public web/API origins and Function host storage initially |
| Cost Optimization | Partial | Budget-governed and measurable | No actual Azure baseline exists yet |
| Operational Excellence | Partial | GitHub-governed and observable | One production environment initially |
| Performance Efficiency | Partial | Static-first and elastic | Cold starts and Cosmos partition design require measurement |
1. Reliability¶
Objective¶
Keep published content readable during backend failures, recover mutable state within the agreed window, and make asynchronous side effects repeatable without duplication.
| Control | Target design | Verification signal | Status |
|---|---|---|---|
| Static failure isolation | Public pages and data are materialized into the frontend artifact | Static HTML per route is written at build (frontend/scripts/prerender.mjs) and article pages prerender from the committed content manifest (publish-content-manifest.yml); the hydrated SPA still reads the API, and no API or Cosmos outage drill is recorded. Read 2026-10-15 |
Partial |
| Zone resilience | Declined on cost: Cosmos and the Function App are single-zone, storage is geo-redundant (RA-GRS content, GRS host), not zone-redundant (ADR 0011 amendment 2026-10-06) | Resource configuration; no zone-failure evidence sought | Decided against |
| Data recovery | Cosmos continuous backup (30 days) plus the nightly out-of-account export (ADR 0028); Blob versioning and soft delete | Restore drill records RTO/RPO; drill run 2026-10-13 (RTO 01:39:04, RPO 22 h, 11,971 docs, PLAT-1 #961) | Verified |
| Retry safety | Idempotency keys, conditional state transitions, bounded retry, poison queues | Jobs are claimed with an etag-conditioned replace and never retried on their own (functions/src/lib/jobs.js); a duplicate delivery runs once (jobs.test.js); five failed handoffs land in platform-jobs-poison, and alert-jobs-poison fired on the 2026-10-15 fire test (#964); change-feed side effects use rising-edge etag claims. The one-effect proof is a unit test, not a live one |
Implemented |
| Deployment recovery | Immutable artifacts and known-good rollback package | Rollback is a reviewed forward deploy (deploy-azure-frontend.yml header; deployment-runbook.md); break-glass rollback uses previous packages in function-releases container; timed rollback rehearsal recorded 2026-10-16 (RTO 08:42 against 15 min objective, PLAT-5 #965); post-deploy smoke tier 1 and frontend probe gates deployments |
Verified 2026-10-16 |
| Dependency resilience | Timeouts, circuit/fallback behavior, non-AI degradation | One outbound fetch helper with a 30 s deadline (http/fetch-with-timeout.js); AI providers fail over in order under a bounded time budget (ai/router.js, ai/time-budget.js, unit-tested); no circuit breaker and no fault injection against live dependencies. Read 2026-10-15 |
Partial |
| Health model | Public synthetic test, admin journey, worker/queue health, dependency telemetry | The edge probe on /api/health feeds alert-api-reachability (armed 2026-09-01); timers, the queue and the host have alert-timer-overdue, alert-jobs-poison and alert-lab-heartbeat; delivery proven by mail and SMS 2026-08-30, and ten of the fifteen rules in the alerting runbook's Fire record were seen to fire by 2026-10-15 (#964); the admin journey runs only in CI against a stubbed identity |
Partial |
Accepted risks¶
- One primary Azure region.
- Cosmos DB Serverless cannot add another region.
- Hostinger remains an external labs dependency.
- Functions Flex has no deployment slots.
These risks are accepted only while the workload remains within the cost-conscious availability target. Sustained usage, business criticality, or a tighter RTO/RPO reopens the hosting and data SKU decisions.
2. Security¶
Objective¶
Use identity as the primary control plane, minimize externally reachable privilege, protect secrets and mutable data, and preserve auditable administrator actions.
| Control | Target design | Verification signal | Status |
|---|---|---|---|
| Administrator authentication | Entra SPA/API registrations and admin app role/group | Every admin handler goes through the role guard (auth/require-role.js, auth/default-guard.js): Entra token, app role and the admins/{oid} record, denials audited; the route inventory test refuses a route that is neither guarded nor on the public allowlist; require-role.test.js proves 403 on a missing or insufficient role. Live denial rows in admin_audit_logs are the production signal and were not read here. Read 2026-10-15 |
Implemented |
| Service authentication | System-assigned managed identity and data-plane RBAC | System-assigned identity with identity-based host storage (infra/functionapp.tf); shared-key access off on both storage accounts (infra/storage.tf; read live 2026-10-15 on the lab backup account and hourly on the host account by monitor-functions-registered.yml); Cosmos local auth off with data-plane role assignment; app-settings-secrets.test.js fails on any literal secret; no key outputs |
Verified 2026-10-15 |
| Deployment authentication | Separate GitHub OIDC identities for plan and apply | User-assigned identities federated to GitHub by subject and split by job (infra/oidc.tf, oidc-subjects.test.mjs); no Azure client secret in GitHub; HCP Terraform's plan and apply share one identity, the split recorded as an option in ADR 0005. Read 2026-10-15 |
Partial |
| Secret protection | Key Vault RBAC, purge protection, 90-day soft delete, diagnostics | Key Vault RBAC, purge protection, 90-day soft delete, default-Deny network and AuditEvent diagnostics (infra/keyvault.tf, infra/observability.tf); alert-kv-config-write fired 2026-10-11, alert-kv-errors and alert-kv-data-write fired 2026-10-15 (#964, the Fire record) |
Verified 2026-10-15 |
| Network access | Flex-delegated integration subnet and selective Private Link for Cosmos, content storage and Key Vault | Service-endpoint firewalls on Key Vault, both storage accounts and Cosmos admit the delegated integration subnet only (infra/network.tf, read live 2026-10-14 on #980); the owner declined Private Link and private DNS on 2026-10-06 (ADR 0031), so the target's private-DNS half is not built, by decision |
Partial, by decision |
| Encryption | TLS 1.2+ in transit and Microsoft-managed encryption at rest; no CMK requirement initially | TLS 1.2 minimum on both storage accounts, HTTPS-only with HTTP/2 on the Function App, HSTS with preload from the Static Web App (staticwebapp.config.json); Microsoft-managed keys by owner decision (ADR 0031); Trivy blocks HIGH and CRITICAL IaC findings in CI; no protocol test runs against production. Read 2026-10-15 |
Implemented |
| Storage exposure | Anonymous blob access and shared-key application access disabled | No nested public items and private containers on both accounts, shared key off by default (infra/storage.tf); the content account's firewall admits the integration subnet only, so an anonymous fetch is refused at the network before any container (its unused localhost CORS entry removed in #1179); public media is served through the app from a container allowlist. No anonymous fetch was tried against production. Read 2026-10-15 |
Implemented |
| API protection | Explicit public route allowlist; Entra on admin; schema validation and rate controls | Explicit public route allowlist with a reason per route (route-inventory.test.js), Entra on admin, body validation per handler, a daily cap with compare-and-increment, rate limiting keyed on a Cloudflare-verified identity that fails closed (auth/client-identity.js), the origin locked to Cloudflare's ranges with assert-origin-lock.mjs reading the live rules after every deploy (held to the published list weekly from #1179) |
Verified 2026-10-15 |
| Audit | Immutable-enough admin and integration audit records with correlation IDs | Admin mutations, denials and integration proxy writes land in admin_audit_logs with action, user, time, user agent and details (content-workflow/shared.js, auth/default-guard.js, integrations/rest-proxy.js since #1167); rows carry no correlation or deployment id and the container accepts upserts. Read 2026-10-15 |
Partial |
| Supply chain | Pinned actions/modules, CodeQL, dependency review, secret/IaC scans, artifact attestations | Actions pinned by SHA and tested to be; CodeQL for JavaScript, Actions and Python; dependency review and Trivy as required checks; GitGuardian's secret check on every pull request; provenance attestation for the lab image only (publish-lab-image.yml); checkov and zizmor run locally through Qlty, not as a CI gate; the Functions and frontend releases carry no attestation. Read 2026-10-15 |
Partial |
Accepted risks¶
- Cloudflare-fronted Azure origins remain publicly addressable initially.
- Isolated Function host storage and Azure OpenAI retain public service endpoints initially.
- API Management, Front Door Premium Private Link, Defender upgrades, and Private Link for every host storage subresource are deferred pending threat and cost evidence.
Authorization at the Azure API remains mandatory even when Cloudflare blocks a request at the edge.
3. Cost Optimization¶
Objective¶
Keep expected monthly Azure spend below USD 150 without trading away the agreed security, recovery, or user experience baseline.
| Control | Target design | Verification signal | Status |
|---|---|---|---|
| Budget | Resource-group budget at USD 150 with 50/75/90/100 percent thresholds | Two subscription-scoped budgets at USD 150 with 50/75/90/100 % actual and a 100 % forecast threshold, routed to the ops action group and mail (infra/budget.tf; subscription scope rather than resource group, by its own comment); test notifications reached mail and SMS on 2026-08-30; the Management budget read applied 2026-10-14 |
Verified 2026-10-14 |
| Allocation | Required tags on all eligible resources | One required tag map applied through local.tags everywhere (infra/variables.tf); no CI or policy check reports coverage, as the cost analysis says. Read 2026-10-15 |
Partial |
| Compute | Flex Consumption, zero always-ready instances initially | Flex Consumption FC1, 2048 MB per instance, maximum 20 instances, always-ready zero by decision (ADR 0031); September billed USD 6.09 (cost analysis); cold-start latency is not measured (infra/functionapp.tf says so). Read 2026-10-15 |
Implemented |
| Database | Cosmos Serverless; RU-aware queries/indexes | Serverless account, explicit index paths and composite indexes per container (infra/cosmos.tf, cosmos-containers.json), alert-cosmos-throttle on 429s; September billed USD 5.71; per-operation and per-partition RU telemetry was pruned for the log cap (#947 tracks RU logging). Read 2026-10-15 |
Implemented |
| Storage | ZRS where recovery value warrants it; lifecycle and short recovery retention | RA-GRS content and GRS host accounts by decision (ADR 0031), not ZRS; 7-day blob and container soft delete; lifecycle rules for noncurrent versions, exports and uploads (infra/storage.tf); September capacity and transactions measured in the cost analysis. Read 2026-10-15 |
Implemented |
| Observability | 30-day workspace retention, sampling, daily cap and cap alerts | 30-day retention, a 0.25 GB daily cap that refuses unlimited (infra/main.tf, infra/variables.tf), adaptive sampling excluding requests (functions/host.json), alert-logs-capacity at 80 % of the cap; September ingestion billed USD 0.00 |
Verified 2026-10-15 |
| AI | Explicit feature flag, TPM and token bounds, dynamic quota off | Foundry deployments capped at 50K TPM with local auth off and a USD 75 budget (infra/foundry.tf); every feature switchable off by an administrator; tokens and estimated cost recorded per call in ai_usage; the first measured Foundry month is pending (#849, November). Read 2026-10-15 |
Implemented |
| Edge | Retain Cloudflare instead of Azure Front Door fixed fee | Cloudflare kept (ADR 0002) against Front Door's fixed fee (cost analysis); DNS and the origin-secret ruleset in Terraform (infra/frontend.tf); the site itself is DNS-only to the Static Web App (apex and www read 2026-10-15), so there is no cache-hit ratio to measure for it, and the Cloudflare plan cost is listed as open |
Implemented |
Potential savings are not realized savings. After deployment, actual billed cost becomes the baseline, and architectural changes are measured against both cost and service-quality indicators.
4. Operational Excellence¶
Objective¶
Make every production change reviewable, repeatable, observable, reversible, and attributable.
| Control | Target design | Verification signal | Status |
|---|---|---|---|
| Infrastructure as code | Thin Terraform roots composed from pinned AVMs | A native-provider root module (ADR 0020 superseded AVM composition); fmt, validate, tflint and Trivy are required checks; terraform test runs for infra-lab/ only; no policy-as-code gate. Read 2026-10-15 |
Partial |
| State | Remote locked production state with version/recovery protection | Remote state in the HCP Terraform hcw-azure workspace, locked and versioned (infra/backend.tf); deploys refuse to run during an apply and fail closed without the token (#1166); recovery is a state version restore in HCP Terraform (deployment-runbook.md); no lock-contention test recorded. Read 2026-10-15 |
Implemented |
| Plan/apply separation | Read-only PR plan and protected production apply | Plans run in HCP Terraform and a human confirms the apply; tfc-plan-check.yml compares a run's plan with the declared permanent diff (assert-expected-plan.mjs) on dispatch; plan and apply share one identity, and nothing proves the applied plan equals the reviewed one. Read 2026-10-15 |
Partial |
| Change safety | Protected branch/environment, CODEOWNERS, concurrency, no self-approval | Ruleset 20680114 with no bypass actors, required status checks, conversation resolution and the production environment gate on deploys; CODEOWNERS exists; zero required approvals and code-owner review off by owner decision while Copilot credits are out (to 2026-10-31; #972). Read 2026-10-15 |
Partial, by decision |
| Application delivery | Build once, scan/test/attest once, promote immutable artifact | The frontend is built once and re-checked from its artifact before deploy; the Functions package is built and deployed in one job; provenance attestation for the lab image only; monitor-deploy-drift.yml names production lagging main. Read 2026-10-15 |
Partial |
| Observability | Per-component App Insights in one Log Analytics workspace | One workspace-based Application Insights component on the central workspace (infra/main.tf); custom events for alertable signals (lib/telemetry.js); a rule table with a meaning and first steps per rule in the alerting runbook; no dashboards, SLO definitions or frontend telemetry component. Read 2026-10-15 |
Partial |
| Incident readiness | Severity model, owner, rollback, restore, dependency runbooks | A severity, a meaning and first steps per rule; restore, rollback and dependency runbooks; the Cosmos restore drill run 2026-10-13 (RTO 1 h 39 min, RPO 22 h) with check-drill-age.yml holding it quarterly; no tabletop or rollback exercise recorded |
Partial |
| Migration evidence | Counts, hashes, query results, routes, and external contract tests | Phase 4 evidence log: document counts reconciled (0 missing, 0 extra), blob MD5 and byte checks, partition keys signed off 2026-08-21 (docs/history/phase-4-data-migration.md); the reverse path and rehearsal estate retired (ADR 0023) |
Verified 2026-08-21 |
One production environment increases change risk. Pull-request preview environments, emulators, local tests, and artifact validation partially compensate, but a persistent staging environment should be reconsidered if change volume or contributors increase.
5. Performance Efficiency¶
Objective¶
Serve public pages rapidly from the edge and scale dynamic work according to measured demand without unbounded cost.
| Control | Target design | Verification signal | Status |
|---|---|---|---|
| Public delivery | Pre-rendered HTML and immutable assets cached at Cloudflare/SWA | Pre-rendered HTML per route; /assets/* immutable for a year and HTML no-cache (staticwebapp.config.json); the deploy fails below 60 prerendered documents; Core Web Vitals, TTFB and cache-hit ratio are not measured. Read 2026-10-15 |
Implemented |
| API scaling | Flex per-function scaling with bounded concurrency | Flex per-function scaling to 20 instances with dynamic concurrency (functions/host.json); alert-func-http5xx and alert-func-latency (P95 over 5 s); from #1179, alert-export-window-latency measures the nightly export's effect on API P95; cold starts are not measured. Read 2026-10-15 |
Implemented |
| Async work | Queue buffering isolates spikes and protects third parties | Long handlers enqueue to platform-jobs and return 202; a stale-queued sweeper re-enqueues; the poison alert counts arrivals; no queue age or depth signal and no completion-time measurement (ADR 0012 asks for both). Read 2026-10-15 |
Partial |
| Cosmos partitioning | Query-contract-led, high-cardinality keys and selective indexes | RU/query, cross-partition rate, hot partitions | Design gate |
| Payload design | Projection, pagination, bounded documents and media in Blob | List routes project a field allowlist with a default limit of 60 and a maximum of 250 (lib/public-reads.js); job payloads capped at 64 KB; media in Blob through the media route; TTLs on cache and counter documents. Read 2026-10-15 |
Implemented |
| AI efficiency | Bounded context/output, caching where semantically safe, fallback | Provider failover, a prompt-cache marker on static system prompts and bounded max_tokens (ai/router.js), a time budget per call; tokens and cost recorded per call; the weekly provider probe re-measures latency; cache-hit rate is not measured. Read 2026-10-15 |
Implemented |
| Frontend bundles | Static/dynamic split, route-level loading, compression | Routes load lazily and vendor chunks are split with the React runtime pinned to its own chunk (vite.config.js); the 456 kB critical path was measured and fixed; no chunk-size report or route budget gate in CI; compression is left to the Static Web App and Cloudflare. Read 2026-10-15 |
Partial |
Cross-pillar tradeoffs¶
| Decision | Benefit | Cost or risk | Revisit trigger |
|---|---|---|---|
| Cloudflare over Front Door | Lower fixed cost; preserves current edge | No Azure Private Link origin integration | Origin-bypass incident or stronger compliance need |
| Serverless Cosmos | Excellent idle/spiky economics | Single-region and migration required for multiregion | RTO/RPO tightens or RU profile becomes sustained |
| Selective Private Link | Private editorial data and secrets | Endpoint/DNS cost; host state remains public | Threat model or budget justifies full host privatization |
| Single Function App (ADR 0019) | Consolidated scaling, single telemetry sink, eliminated cold starts | Shared execution runtime across functions | Operating burden or security boundary requires physical isolation |
| One production environment | Lowest fixed cost and simplest state | Reduced preproduction fidelity | Higher change volume or multiple contributors |
| ZRS storage | Zone-level durability | Higher cost than LRS | Actual asset recovery value is low or budget pressure persists |
Definition of architecture complete¶
Architecture approval requires:
- no missing source capability domain;
- explicit identity, network, data, recovery, observability, cost, and delivery boundaries;
- a named owner and measurable signal for every material control;
- accepted risks and revisit triggers;
- approved resource topology and infrastructure plan;
- no unresolved choice that would materially change Terraform state or production exposure.
Implementation completion requires separate evidence. This document alone does not claim that any Azure control is deployed or verified.