Skip to content

Azure Hosting Cutover

Daniel Truong edited this page Oct 3, 2026 · 18 revisions

Azure hosting — eagle-public and eagle-admin

Reference page for the Azure Front Door edge, repo digitalspace/eagle-edge. Both estates serve the eagle-public Angular bundle from Front Door and hold a separate origin group per app: Front Door talks to eagle-api and DEMI directly. Both estates also serve the eagle-admin console from storage. rproxy is not in the path for app traffic on either estate. See Front Door Per-App Split for the origin groups, routes and patterns, which are the same on both estates.

Test also hosts DEMI's frontend and admin endpoints on the same profile.

Not the same thing as the search backend move; see Eagle Search for that.

Request path

Prod:

browser → Front Door (eagle-edge-prod) → route eagle-api  → eagle-api-6cdc9e-prod.apps.silver.devops.gov.bc.ca
                                        → route admin      → storage static website (eagle-admin console, `$web/admin/`)
                                        → route demi/demi-cached, usage, analytics → demi-apim-prod.azure-api.net
                                        → route default    → storage static website (eagle-public bundle)

www.projects.eao.gov.bc.ca resolves the same way.

Test:

browser → Front Door (eagle-edge-test) → route eagle-api  → eagle-api-6cdc9e-test.apps.silver.devops.gov.bc.ca
                                        → route admin      → storage static website (eagle-admin console, `$web/admin/`)
                                        → route demi/demi-cached, usage, analytics → demi-apim-test.azure-api.net
                                        → route default    → storage static website (eagle-public bundle)

Neither eagle-test.apps.silver.devops.gov.bc.ca nor eagle-prod.apps.silver.devops.gov.bc.ca — the OpenShift routes that fronted app traffic through rproxy — are in this path. See Front Door per-app split.

Two profiles

One Bicep template, azure/main.bicep, deploys a differently-shaped profile per estate.

Test Prod
Resource group c4b0a8-test-rg rg-eagle-public-prod
Subscription 7897ceb1-… be5924ac-1083-4a1b-be92-7b444882cfd9
Front Door profile eagle-edge-test eagle-edge-prod
eagle-public endpoint eagle-public-test-dbg8ghh8gjd0bscx.a02.azurefd.net eagle-public-prod-aafug4ahavgzbvh9.a01.azurefd.net
Storage ($web) eaglepubtestvymaysch2agd eaglepubprodrhj3zycszo6j (.z9.web.core.windows.net)
eagle-admin storage ($web/admin/) eagleadmtestvymaysch2agd eagleadmprodrhj3zycszo6j
Also on this profile DEMI frontend + DEMI admin endpoints (own storage, own CSP) nothing else
Backend origin per-app: eagle-api-6cdc9e-test..., the eagle-admin storage static website, demi-apim-test.azure-api.net — see Front Door Per-App Split per-app: eagle-api-6cdc9e-prod..., the eagle-admin storage static website, demi-apim-prod.azure-api.net — see Front Door Per-App Split
Custom domains test.projects.eao.gov.bc.ca, www.test.projects.eao.gov.bc.ca projects.eao.gov.bc.ca, www.projects.eao.gov.bc.ca
WAF / security policy eagleedgetestwaf / sp-test eagleedgeprodwaf / sp-prod
Shared identity, Log Analytics, App Insights eagle-search-identity-test, eagle-search-logs-test, eagle-search-insights-test (misnamed, shared with demi-api and eagle-search-api — never rename) none (another subscription); prod's own Log Analytics/App Insights pair belongs to the availability test

Endpoint hostnames are <name>-<hash>.<zone>.azurefd.net with both the hash and the zone assigned by Azure at deploy time — this profile came back a02/a01, not a z01 you might expect from older docs. Read them from the edgeEndpointHostNames deployment output or epic_azure_status, never compose one. A wrong zone code still resolves through that zone's wildcard and returns 404 for every page even though the deployment reports success.

Custom domains and WAF

Both estates declare their public hostnames as Front Door custom domains with a managed TLS certificate (TLS 1.2 minimum). Each stays state Pending, and serves no traffic, until its _dnsauth TXT record exists in Province DNS and the hostname's own DNS record points at the endpoint — declaring a domain moves nothing on its own. Public DNS for both hostnames resolves to the Front Door endpoint directly.

A WAF policy (Standard_AzureFrontDoor SKU, custom rules only, no managed rule set) attaches to the custom domains, not to the *.azurefd.net endpoint, so rate limiting sees the browser's own address rather than a single upstream proxy. Three rate-limit rules, keyed on socket address, 1-minute window, Prevention mode:

Rule Path prefix Threshold / min
search /demi-search/ 600
analytics /analytics 1200
general /api/ 1800

Rule set propagation

A rule set change reaches Front Door's edge PoPs after the ARM deployment reports Succeeded, up to ~3 minutes later (measured 2026-08-15 at 75s and 2m36s on two separate deploys). A single check right after a deploy proves nothing; poll. Until the rules land, a deep link falls through to storage's 404 document — same bytes, 404 status:

curl -o /dev/null -w '%{http_code}' https://<endpoint>/projects/deadbeef   # want 200

Purge fixes neither this nor the App Service CORS window described in the eagle-edge README — both are config lag, not cached content.

Publishing the bundle

Workflow Deploy eagle-public to Azure production (LIVE SITE) (deploy-azure-prod.yaml on develop), workflow_dispatch with a version tag verified on test:

gh workflow run "Deploy eagle-public to Azure production (LIVE SITE)" --repo bcgov/eagle-public -f version=v2.7.28

It builds from the tag, uploads to $web, purges the edge, then smoke-tests. Environment azure-prod gates the run on a reviewer. Concurrency group deploy-azure-prod, cancel-in-progress: false.

Staging is Deploy eagle-public to Azure staging (deploy-azure-staging.yaml), same input, target c4b0a8-test-rg endpoint eagle-public-test-dbg8ghh8gjd0bscx.a02.azurefd.net.

Headers as served:

File Cache-Control
index.html, env.js no-cache, no-store, must-revalidate
*-[hash].js, *.css public, max-age=31536000, immutable
assets/* no-cache (not content-hashed; angular.json sets outputHashing: 'bundles')

The workflow asserts Cache-Control by reading the blob back. A manual az storage blob upload-batch must pass --content-cache-control or it silently strips the header; the command exits 0 on precondition failures.

A stuck run: with cancel-in-progress: false, a run left in waiting (environment approval never given) blocks every later run in the group. Later runs sit in pending with pending_deployments empty. Cancel the stale waiting run. pending = queued behind concurrency; waiting = approval gate.

gh api "repos/bcgov/eagle-public/actions/workflows/335990413/runs?per_page=20" --jq '.workflow_runs[] | "\(.id) \(.status)"'

Security headers come from the Front Door securityheaders rule set, not from the pod nginx. The AFD CSP adds base-uri 'self'; frame-ancestors 'none' and X-Frame-Options: DENY match the pod policy.

Deploying the edge (Bicep)

A merge to main deploys test. Prod deploys only from a v* tag: gh workflow run deploy.yaml --ref vX.Y.Z -R digitalspace/eagle-edge. Read ./scripts/deploy-infra.sh prod (what-if) locally first. See Deployment Pipeline.

An incremental deploy never deletes a route or rule set that a param change stops declaring — it keeps holding the patterns the new routes claim. Delete it by hand first with az afd route delete / az afd rule delete, then deploy.

  • An ABAC condition on the c4b0a8 management group Owner assignment denies roleAssignments/write for Owner, Contributor, User Access Administrator, RBAC Administrator and two custom roles. Nobody in the subscription can grant Contributor to the CI identity. roleDefinitions/write is not denied; CI identity eagle-edge-cicd-prod holds custom role eagle-edge-deployer-prod, created by scripts/bootstrap-cicd.sh prod in eagle-edge.
  • What-if always reports one Modify on originGroups/og-eagle-public/origins/storage: hostName and originHostHeader "change" to an unevaluated reference() expression. What-if cannot resolve runtime reference(). The real value does not change.

SPA fallback is the AFD spafallback rule, keyed on UrlFileExtension LessThanOrEqual 0. An Angular route containing a dot returns 404 with a page that still renders. Status-only checks and crawlers see the 404.

Availability monitoring

eagle-public-availability-prod GETs https://projects.eao.gov.bc.ca/ every 300 s from five North American locations and fails when the body lacks <app-root>. It targets the public hostname, not the AFD endpoint, and checks content because a 200 status does not prove the app rendered. It runs no JavaScript, so it proves the shell is served, not that the app boots.

Alerting is off: availabilityAlertEmail is empty (main.prod.bicepparam reads AVAILABILITY_ALERT_EMAIL from the deploying shell), so no action group and no metric alert exist. Results are recorded and nobody is notified. To turn it on, export AVAILABILITY_ALERT_EMAIL and redeploy the edge. Restore any hand-edited test setting by redeploying the template, not by PATCH.

publicNetworkAccessForIngestion on the Insights component must stay Enabled. The probe fleet writes over the public internet and there is no AMPLS; Disabled fails silently.

Verifying the live site

  • curl -sI https://projects.eao.gov.bc.ca/ → 200 with x-azure-ref, not a 301.
  • index.html → no-store; a hashed asset → immutable; AFD x-cache: TCP_HIT on hashed assets.
  • /api/config → 200 from eagle-api.
  • A deep link → 200; a dotted route → 404 (expected).
  • www.projects.eao.gov.bc.ca → same as apex.

Rollback

Both estates: see Front Door Per-App Split § rproxy retirement for scaling rproxy back up and reverting the edge routes.

Prod: see also Rollback Procedures § eao-nginx. For eagle-admin on test and prod, see Rollback Procedures § eagle-admin on test and prod.

Test: oc scale deploy rproxy -n 6cdc9e-test --replicas=2 restores rproxy; see Front Door Per-App Split for reverting the edge routes.

Related

Clone this wiki locally