Website guts, 2026 edition.
The previous version of this site ran on a Raspberry Pi in a bedroom. This one doesn’t run anywhere, which is rather the point.
The shape of it
Every page you’re reading was rendered to a file before you asked for it. There is no application server, no database, and no Python executing anywhere on the internet. The whole site is a folder of HTML sitting in an S3 bucket, handed out by CloudFront from whichever edge location is nearest you.
Route 53 → CloudFront → S3 (private)
↑
CloudFront Function
(routing, redirects, index files)
That’s the entire architecture. Four boxes, and one of them is a DNS record.
Why nothing dynamic
The honest answer is that the old site didn’t need it either. It was a Flask app with four routes, a SQLite database containing one empty table, and not a single form. Flask was doing an elaborate job of returning the same bytes every time. Cutting it out removed a web server, a WSGI middleware layer, a database, and about 400ms of cold-start latency, and cost exactly nothing in features.
If a contact form or a leaderboard ever earns its place here, it arrives as one small function
behind /api/, and the rest of the site doesn’t change.
The pieces
S3 holds the built site. The bucket is private — no public reads, no static website hosting. CloudFront reaches it through an Origin Access Control, which is a signed relationship between the distribution and the bucket. Requesting the bucket URL directly gets you a 403.
CloudFront is the CDN, and on the flat-rate Free plan it also brings AWS WAF, DDoS protection, a TLS certificate, and the Route 53 hosted zone along with it. Flat rate means no overage charges, which matters more than the price: the failure mode of a viral link or a determined attacker is a slow month, not a surprise invoice.
A CloudFront Function handles the small routing jobs at the edge, in about a millisecond. It
redirects www to the bare domain, forwards the old site’s URLs to their new homes, and appends
index.html to directory requests. That last one is load-bearing: a private S3 origin speaks the
REST API, which has no notion of a default document, so /resume/ would otherwise return a 403
rather than a page.
Terraform describes all of it. The reason isn’t scale, it’s memory. Console clicks are impossible to review and impossible to remember eighteen months later, which is roughly the interval at which I touch this site.
A build script turns Markdown into HTML. It’s a hundred-odd lines of Python and Jinja2 — the
same templating engine the Flask version used, minus Flask. Every asset filename contains a hash of
its own contents, so stylesheets and images can be cached for a year and still update instantly when
they change. Deploys happen on git push, through GitHub Actions authenticating to AWS by OIDC, so
there are no AWS keys stored anywhere.
What it costs
Near enough to nothing. The CloudFront Free plan covers a million requests and 100 GB of transfer per month at $0, and bundles the DNS zone and certificate that would otherwise be the only recurring line items. A portfolio site is not going to trouble those numbers.
So the bill is the domain registration, about $20 a year, and that’s the whole story. Roughly what the Raspberry Pi version cost, without the Raspberry Pi.
What was actually lost
Some fun, honestly. The old setup had a physical object with blinking lights, and a real answer to “where is your website” that you could point at. Debugging meant SSH’ing into a thing on a shelf. There’s a directness to that which a managed CDN does not replicate.
What I gained is that it stays up when the power flickers, it doesn’t care if Comcast rotates my IP address, and it will still be serving pages in three years if I never log in again. For something that exists to be read occasionally by strangers, that trade is obviously correct — but it isn’t free of cost, and the Pi version was more interesting to build.