Frontend API base URL guard + cloud env recovery¶
Date: 2026-05-21
Scope: scripts/dev/quick-deploy.sh, deployed Container App ca-elb-control (frontend env)
Motivation¶
The cloud dashboard at
https://ca-elb-control.gentlemeadow-01289e5b.koreacentral.azurecontainerapps.io
showed every monitoring card as Network error and the subscription
selector as Error. Root cause: the frontend container in revision
--0000110 had
baked into its environment. web/entrypoint.sh writes that value into
/runtime-config.js, so the browser-side SPA was issuing every /api/*
call against the operator's own laptop (http://localhost:8085), not
the Container App. That also explained the stale dropdown values
(rg-elb-01, elbacr01 · rg-elbacr-01, elbstg01 · rg-elb-01) — those
came from the operator's local dev API working against a different
environment. /api/me returned 401 because the MSAL token issued for the
cloud client id was rejected by the local API.
How the poisoned value got there: scripts/dev/local-run.sh web
exports VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8085}
in the calling shell. Running scripts/dev/quick-deploy.sh frontend in
the same shell carried that export into the build args + the
az containerapp update --set-env-vars patch.
User-facing change¶
- Cloud dashboard cards (AKS, ACR, Storage, Terminal, Subscription/RG
selectors) now reach the same-origin backend again. No code change to
the SPA — fixing the container env was sufficient because
runtime-config.jsis generated at container start. quick-deploy.sh frontendnow refuses to run ifVITE_API_BASE_URLpoints atlocalhost,127.*,0.0.0.0, or[::1], with the message:
VITE_API_BASE_URL='http://localhost:8085' points at the local host — refusing to bake that into the cloud frontend. Run 'unset VITE_API_BASE_URL' (or export VITE_API_BASE_URL='') and retry. *
quick-deploy.shno longer inheritsVITE_API_BASE_URLfromweb/.env.local. That file is the local-dev convention for the Vite dev server (vite dev+local-run.sh web) and pins the value tohttp://localhost:8085. The loader forweb/.env.localnow accepts a skip-list, andquick-deploy.shpassesVITE_API_BASE_URLto it. *web/nginx.confnow serves/runtime-config.jswithCache-Control: no-store, must-revalidate. Previously the file went out with noCache-Control, so browsers applied heuristic disk caching and kept the poisoned config even after the cloud env had been fixed.
API / IaC diff¶
- No API surface change.
- No Bicep change.
scripts/dev/quick-deploy.sh:- Regex guard in the
SIDECAR == "frontend"branch rejects loopback values forVITE_API_BASE_URL. load_simple_env_filenow accepts a skip-list of variable names;web/.env.localis loaded withVITE_API_BASE_URLin the skip-list so the cloud build never inherits the dev-loopback value.web/nginx.conf: dedicatedlocation = /runtime-config.jsblock addsCache-Control: no-store, must-revalidateandexpires off.- Cloud env patched out-of-band (already shipped earlier in the same session, before the image rebuild):
az containerapp update -n ca-elb-control -g rg-elb-ca \
--container-name frontend --set-env-vars VITE_API_BASE_URL=
acrelbnm5virmqrdi5c.azurecr.io/elb-frontend:20260521231605
rolled out as ca-elb-control--0000112.
Validation¶
- runtime-config.js (before):
{"VITE_API_BASE_URL":"http://localhost:8085", ...}— broken. az containerapp updateissued at 14:01 UTC, revisionca-elb-control--0000111reportedlatestReadyRevisionNamewithin ~12 s.- runtime-config.js (after env patch, revision --0000111):
{"VITE_API_BASE_URL":"","VITE_AUTH_DEV_BYPASS":"false",...}— same-origin restored. - Guard smoke test:
bash -n scripts/dev/quick-deploy.sh: syntax OK.load_simple_env_fileskip-list smoke test: after the cleanup, runningquick-deploy.sh frontendwithVITE_API_BASE_URLunset in the shell no longer reintroduceshttp://localhost:8085fromweb/.env.local— the guard does not trip, build proceeds.- Frontend image rebuild + rollout: tag
20260521231605, revisionca-elb-control--0000112becamelatestReadyRevisionNamewithin ~10 s. - runtime-config.js (after rebuild, revision --0000112):
Body still reports
VITE_API_BASE_URL=""(same-origin). - /index.html headers unchanged:
cache-control: no-cache(matches the existinglocation = /index.htmlblock).
Follow-up¶
- Existing user tabs that still hold the cached
runtime-config.jsbody need a hard reload once. From the next deploy onwards, theno-storeheader prevents this class of staleness. - If a similar poisoned env slipped into the
api/worker/beatcontainers, future deploys would also propagate it. Those containers do not readVITE_*, so the immediate blast radius is limited to the SPA.