Cloud engineer · Beirut

Small systems,
observed closely.

I build small, complete systems and write down how they actually work. This page runs on the architecture it describes — and counts you.

Hadi Itani
0
LIVE visitors · counted by an
Azure Function + Cosmos DB

Selected work

2026 —

An end-to-end cloud build on Azure: a static frontend, a Python serverless API, a NoSQL database, and a CDN/TLS layer — wired together and deployed to a custom domain. The point was never the resume; it was designing, building, and operating a small but complete system, with everything defined as code and no long-lived secrets.

It reports its own traffic: a visitor counter backed by an Azure Function and Cosmos DB, behind Cloudflare. The Function reaches Cosmos passwordless — a managed identity with an RBAC role, no connection string anywhere.

DEMONSTRATES — serverless API · passwordless (managed identity + RBAC) · Bicep IaC · OIDC pipelines · live telemetry

Python — Azure Functions — Cosmos DB — Cloudflare — Bicep Open the case study →

A full-stack reading app with an automated article-ingestion pipeline, on-demand AI summaries, and authenticated, protected routes — built on the Next.js App Router with a Postgres backend.

Next.js — TypeScript — Supabase / Postgres — Clerk Auth — OpenAI API

About

I like the parts that have to work.

I'm a computer science graduate based in Beirut who likes taking a system all the way from an empty cloud account to something running in production. Infrastructure, the service in the middle, and the interface on top — I'm comfortable across the whole path, and I care most about the parts that actually have to work.

The work I enjoy most is the unglamorous part: a deploy that finally goes green, an auth flow that stops needing a stored secret, a piece of plumbing that clicks into place. That's usually where the real engineering is.

I studied Computer Science at the Lebanese American University and hold the Microsoft Azure AI Fundamentals (AI-900) certification.

PythonTypeScriptJavaScriptSQL AzureCosmos DBBicepGitHub Actions Next.jsReact
Illustrated portrait of Hadi at home

Cloud · serverless · 2026

The Observable System

A personal resume site built on Azure as a full, end-to-end cloud project — static frontend, serverless API, NoSQL database, CDN/TLS layer — wired together and deployed to a custom domain. The goal was not the resume. It was operating a small but complete system.

Visitor → Cloudflare (DNS · TLS · CDN · proxy)
                       │  Custom domain on storage, Full TLS
                       ▼
            Azure Storage static site ($web)
  main.js  ── POST /api/counter ──▶  Azure Function (Python)
                                          │ read → increment → write
                                          ▼
                                     Cosmos DB (NoSQL)

How the counter works

A visitor loads the page; Cloudflare serves the cached static site from Azure Storage. main.js issues a POST to the Function's /api/counter endpoint. The Function reads a single document { id: "1", count: N } from Cosmos DB, increments it, writes it back, and returns the new value as JSON. A sessionStorage flag stops the same session re-incrementing on refresh — so the number reflects visits, not reloads.

Trade-offs worth calling out

Cloudflare over Azure Front Door. Azure Storage static sites don't serve a custom domain over HTTPS on their own — you need something in front to terminate TLS. I chose Cloudflare for DNS, free TLS, and a global edge cache in one place, rather than standing up a heavier managed front-end for a small static site. The real work was getting the storage origin to accept the proxied request — the host header didn't match what the storage account expected; registering the custom domain on the storage account lined it up.

Passwordless to Cosmos. The Function reaches Cosmos with a system-assigned managed identity and a data-plane RBAC role — no connection string, no key in the code, config, or pipeline. Everything as code. Storage, Function, Cosmos, the identity, and the role assignments are all declared in Bicep, parameterized for dev/prod, and shipped by two GitHub Actions pipelines that authenticate to Azure with OIDC.

FrontendHTML · CSS · vanilla JS
HostingAzure Storage static website ($web)
DNS / TLS / CDNCloudflare (free tier)
APIAzure Functions · Python 3.11
DatabaseAzure Cosmos DB for NoSQL (serverless)
Auth (Fn → Cosmos)System-assigned managed identity · RBAC
IaC / DeployBicep · GitHub Actions (OIDC)

Build log

Building a resume that runs on the cloud it describes

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.