What I built, the decisions behind it, and the trade-offs I made along the way. · Built over about two weeks, 2026
Why I did this
I wanted a project I could actually defend — not a tutorial I followed, but a system I built myself, broke a few times, and fixed. The Cloud Resume Challenge gave me the skeleton: get a resume onto the cloud, give it a real visitor counter, wire up actual infrastructure behind it. The part that made it mine was a rule I set at the start — everything defined as code, and no long-lived secrets anywhere in the system. It sounds like a small constraint, but it shaped nearly every decision that followed.
The shape of it
The system is deliberately small, and that's a feature. A single request tells the whole story:
Your browser → Cloudflare → an Azure Function → Cosmos DB → back to you
When you loaded this page, a little JavaScript called an API. That call hit Cloudflare first (DNS, TLS, edge caching), got passed to a Python Azure Function, which read the current visitor count out of Cosmos DB, added one, wrote it back, and returned the new number. The count on this site is real — it's the system reporting on itself.
- Frontend — hand-written HTML, CSS, and vanilla JavaScript; no framework, no build step. Static files on Azure Storage, behind Cloudflare.
- API — a Python Azure Function, HTTP-triggered, Flex Consumption.
- Data — Azure Cosmos DB, serverless, one counter document.
- Auth — the Function reaches Cosmos with a managed identity; no connection string, no key.
- Infrastructure — all defined in Bicep, modular, parameterized for dev/prod.
- Deployment — two GitHub Actions pipelines, authenticating to Azure with OIDC.
The front door: Cloudflare over Azure Front Door
Azure Storage static websites don't serve a custom domain over HTTPS on their own — you need something in front to terminate TLS and handle the domain. The default Azure answer is Front Door or Azure CDN, and I looked at that first. I went with Cloudflare instead: it gave me DNS, free TLS on the custom domain, and a fast global edge cache in one place, without standing up a heavier managed front-end for what is a small static site.
The part that took real work was getting the storage origin to accept the request properly through the proxy — the host header wasn't matching what the storage account expected. The fix was registering the custom domain on the storage account itself so the host header lines up. Once that clicked, the front door was solved.
Going passwordless
In the first version, the Function talked to Cosmos with a connection string — the normal way, and where most walkthroughs stop. It bothered me: it's a secret, and secrets are liabilities. So I pulled it out entirely and switched to a system-assigned managed identity, granted a data-plane role on the Cosmos account through RBAC. The Function now requests a token at runtime; there's no secret in the code, the config, or the pipeline.
The thing I had to actually understand was the difference between management-plane and data-plane access in Cosmos — the built-in data-contributor role is what lets the identity read and write documents without touching account administration. There's also a propagation delay after the role is assigned before it takes effect, worth knowing so you don't assume it's broken when it doesn't work in the first few seconds.
The self-healing counter
The Function doesn't assume the counter document already exists. If it's missing — fresh database, or it's been reset — it creates it starting at one instead of erroring. A small piece of defensiveness, but it means the system can come up from nothing without me hand-seeding any data. On the frontend, a sessionStorage flag means refreshes within a session don't re-count — I decided "a visit" means a new session, not every page load.
Two layers of tests
I wrote unit tests that mock the database and check the increment logic in isolation — fast, run on every push before anything deploys. But a mock can't catch a broken Cosmos connection, a missing RBAC role, or a CORS misconfiguration, because it never touches the deployed system. So there's a second layer: smoke tests that hit the actual live API after each deploy, and one of them calls the endpoint twice to confirm the count genuinely moved in the real database. That's the test that gave me confidence the managed-identity switch worked end to end — not "the deploy finished," but "a real request just read and wrote Cosmos with no secret involved."
One honest side effect: those smoke tests and the readiness probe are themselves why the counter ran ahead during development — every deploy fired real requests at the real database. Separating that test traffic from genuine visitors is exactly what the dev/prod split in the infrastructure is there to enable.
Everything as code, and push is the deploy
None of this was clicked together in the portal and left there. The storage, the Function, Cosmos, the identity, the role assignments, and the monitoring are all declared in Bicep, in modules, parameterized so dev and prod can stand side by side. I can tear the whole stack down and rebuild it identically. The part that genuinely shifted how I think was defining the RBAC role assignment itself in code — authorization as infrastructure, not a manual step you do once and forget. That meant being careful about ordering, too: the role assignment depends on the Function's identity existing first, so the templates have to express that dependency or the deploy races itself.
Two pipelines do the work. The frontend one syncs the static files to Storage and purges the Cloudflare cache. The backend one runs the unit tests as a gate, deploys the Bicep, deploys the Function, waits for it to report healthy, then runs the smoke tests against the live endpoint. Auth to Azure is OIDC — GitHub gets a short-lived token, so there's no stored Azure credential in the repo. I also gave the Function a stable custom domain so the frontend always calls a fixed address instead of an auto-generated hostname.
What I'd do next
The honest backlog: real alerting on the Function — latency and failures surfaced as alerts, not just sitting in logs. Finish isolating test traffic from genuine visitors so the counter only ever reflects real ones. And keep writing these down as I go — the writing is where the decisions actually get examined.