Repository navigation
Say what deploying to Azure actually took - #37
Conversation
PolicyPal now runs on Azure. The runbook that was meant to guide that would have failed four times over, so it is rewritten from what worked, and ADR 0023 records the four choices the subscription forced rather than the ones it originally reasoned its way to. The runbook's gaps: --public-access None while the seed restores from a laptop, an app and job created from a :bootstrap image nothing builds, no AcrPull for the managed identity, and a Static Web App whose token it needs but never creates. It now also names the Free Trial limits that shape the topology and the two client-version traps that cost real time. ADR 0023 keeps its reasoning and gains an amendment: Central US because Postgres is restricted in four regions, a shared Container Apps environment because a Free Trial allows one per subscription, a public endpoint behind a firewall because a consumption environment has no VNet path to a private server, and one always-on replica instead of scale-to-zero at about $70 a month. Its SBC chunk count is corrected to 35,756; the old figure predated the ADR 0020 refresh. Verified rather than asserted: counties in 0.43s, 401 unauthenticated, a cited answer in 13s, the migration job green, and data/ byte-identical before and after at 2,247 files and 1,593 PDFs.
|
|
||
| **The server is public, behind a firewall — not `--public-access None`.** An | ||
| earlier version of this runbook said otherwise and was self-contradictory: the | ||
| seed in step 5 restores from your laptop, and a consumption Container Apps | ||
| environment has no VNet path to a private server. Public plus two firewall | ||
| rules is what actually works. Azure enforces TLS regardless. | ||
|
|
There was a problem hiding this comment.
This PR correctly documents that the production database is now public behind a firewall (also see ADR 0023's "database is public behind a firewall, not private"), but README.md#L363-L364 still says:
"Migrations run as a Container Apps job on the exact image digest being deployed, so the database needs no public opening."
That's now factually wrong and contradicts this runbook. CLAUDE.md requires: "Update README.md whenever setup, usage, architecture, or workflows change." Please update that README sentence to match the corrected architecture.
| ``` | ||
|
|
||
| Measured on an M-series Mac under QEMU: **270s total** — `uv sync` 118s, both | ||
| models baked 56s, push 158s. Emulation overhead was far smaller than expected, | ||
| because the build is dominated by network and disk rather than CPU. The result | ||
| is **639 MB compressed** in the registry. Confirm the platform before | ||
| deploying — an arm64 image fails at pull time with an unhelpful message: |
There was a problem hiding this comment.
The build-time breakdown doesn't add up: uv sync 118s + models 56s + push 158s = 332s, not the stated 270s total. Either the total or one of the components is wrong. The same 270s figure is repeated in ADR 0023's "Two smaller things worth knowing" section, so whichever number is correct should be fixed in both places.
| az postgres flexible-server parameter set \ | ||
| --resource-group policypal-rg --server-name policypal-db \ | ||
| --name azure.extensions --value VECTOR | ||
|
|
||
| az postgres flexible-server firewall-rule create \ | ||
| --resource-group policypal-rg --server-name policypal-db \ | ||
| --name AllowAzureServices \ | ||
| --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 | ||
| ``` |
There was a problem hiding this comment.
The --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 rule is Azure's documented "Allow public access from Azure services" special case — it admits connections from any Azure-hosted resource in any subscription or tenant, not just this project's own resources. Here and in ADR 0023 this is described as just "Azure services," which reads narrower than it is. Since only the admin password then stands between the database and anyone who can run code on Azure, it's worth stating the actual scope explicitly (and considering scoping to the Container Apps environment's outbound IP instead).
Problem
PolicyPal is now deployed to Azure. Two documents describe that deployment and
both were wrong.
docs/runbooks/deploy.mdwould have failed four times before reaching arunning app:
--public-access None(deploy.md:33) while step 6 restores the seedfrom a laptop — and a consumption Container Apps environment has no VNet
path to a private server either. As written, nothing could reach the
database.
:bootstrapimage (deploy.md:52, :58) that the runbook never builds.Both
az containerapp createandjob createfail on it.AcrPullgrant. The app and job get system-assigned identities andno permission to pull, so the revision never starts.
required in step 5 and by
cd.yml.ADR 0023 reasoned its way to scale-to-zero, a private database, a dedicated
environment and an unnamed region. The subscription permitted none of those.
Implementation
Documentation only — no code changes.
docs/runbooks/deploy.mdrewritten from the commands that actually ran.It now opens with the four Free Trial limits that shape the whole topology,
each found by hitting it:
eastus,eastus2,westus2,southcentralus"Provisioning is restricted in this region"MaxNumberOfGlobalEnvironmentsInSubExceededTasksOperationsNotAllowed—az acr buildunusablePlus the two client-version traps:
pg_dump15 refuses a 16 server, and anApple Silicon
docker buildproduces arm64 that Container Apps rejects.ADR 0023 keeps its original reasoning and gains a What deploying it
actually changed section covering the four forced decisions, with the reason
each was forced. Its SBC chunk count is corrected 31,359 → 35,756; the old
figure predated the ADR 0020 robots.txt refresh.
CHANGELOG gains the deployment and the seed under Added, and these
corrections under Changed.
Validation evidence
The estate, verified with real requests rather than assumed:
/api/counties?zip=33101/api/auth/meHcg Glossary Coinsurance 67% matchOriginheaderaccess-control-allow-originmatches exactlyThe cited answer is the load-bearing check: it can only succeed if both models
loaded from the image under
HF_HUB_OFFLINE=1and the BM25 index loadedfrom Postgres (ADR 0021).
Restore, every count matching source, zero errors:
uv run ruff check .passes.Security / DB / API impact
data/checksummed before and afterthe whole deployment:
a498f84b…both times, 2,247 files, 1,593 PDFs. Onlythe database crossed the wire; no PDF was uploaded anywhere.
live database password, the production JWT secret and the OpenAI key — all
absent. The runbook shows shell variables (
"$PGPASS"), never values. No.envstaged;.envremains gitignored.secretref:. The JWT secret is freshly generated, not the dev value. Theregistry's admin user stays disabled; the app pulls via managed identity.
This is a genuine weakening versus ADR 0023's intent, forced by the
environment limit, and is documented as such rather than quietly adopted.
TLS is enforced by Azure.
Note for the reader
cd.ymlhas never run. The estate was built by hand, so the workflow's owncorrectness is still unproven; the migration job was executed manually to
de-risk the step most likely to fail. Wiring up the GitHub Environment,
federated credentials and variables remains to be done, and the runbook lists
exactly what they are.