How this site ships

Everything below is checkable. The commit shown is the commit that produced this page, the uptime figure comes from a prober that fetches this URL over the public internet, and the cost table is the actual AWS bill. Both repositories are public.

This build

Captured by the Actions runner at build time and baked into the page.

Commit
b518dbb
Branch
main
Built
2026-07-29 17:43:06 UTC
Pipeline run
deploy #1
Triggered by
jallyn18

Right now

Visits
unavailable
Status
no probe data
Availability
collecting
Response time
average
AWS spend
collected daily

Architecture

Request and data flow for this siteVisitors reach a CloudFront distribution over HTTPS. Requests for pages are served from a private S3 bucket; requests to /api/ are served by a Lambda Function URL. Both origins are private and reached through Origin Access Control. The Lambda reads and writes a DynamoDB table. Two EventBridge-scheduled Lambdas populate that table: an uptime probe that fetches the public URL every five minutes, and a daily collector that queries the Cost Explorer API.probes the public URLVisitorCLOUDFRONTDistributionTLS 1.2+ · HTTP/3 · HSTS · CSPEdge functionindex.html rewrite/*/api/*Amazon S3private bucket · OAC signedLambda Function URLAWS_IAM auth · OAC signedDynamoDBvisits · uptime · costEVENTBRIDGEUptime probeevery 5 minutesCost collectordailyCost Explorer API$0.01 per requestBoth origins are private. The only path to either one is through the distribution.

A single CloudFront distribution fronts two private origins. Page requests go to an S3 bucket that has no public access and no bucket ACLs; requests to /api/* go to a Lambda Function URL set to AWS_IAM auth. CloudFront reaches both through an Origin Access Control that signs every request with SigV4, so neither origin can be addressed directly — a request to the bucket URL or the function URL returns 403.

Mounting the API on the same distribution as the site means the browser makes same-origin requests. That removes CORS preflights, a second certificate, and API Gateway from the design entirely.

What happens when I push

  1. Site repository. Push to main triggers a build. The workflow assumes an AWS role via OIDC, reads the bucket name and distribution id from SSM Parameter Store, builds the site with the commit metadata injected, syncs to S3 in two passes (hashed assets get a one-year immutable cache, HTML and page data get revalidated every time), invalidates the distribution, and then fetches the live URL to confirm the new commit is actually being served.
  2. Infrastructure repository. Pull requests run fmt, validate, and a plan. Merges to main apply the plan artifact the pull request produced rather than re-planning, so what was reviewed is what gets applied.
  3. Every day. A scheduled read-only plan compares the live account against the committed code and opens an issue if they have diverged.
  4. Every week. Trivy scans the Terraform for misconfigurations and the dependency tree for CVEs; gitleaks scans history for committed secrets. Findings land in the Security tab.

Decisions worth defending

No AWS access keys, anywhere

GitHub Actions authenticates with OIDC and assumes a role whose trust policy names a specific repository and ref. The only value stored in GitHub is a role ARN, which is not a secret. There is no key to rotate and nothing to leak in a workflow log.

The two repositories get different roles. The site repository's role can write to one bucket and invalidate one distribution; it cannot read Terraform state, touch IAM, or change infrastructure. Fork pull requests cannot assume either role.

One role per Lambda

Three functions, three roles. The uptime prober can write to DynamoDB but cannot call Cost Explorer; the cost collector cannot read the visit counter. Sharing a single role would have been fewer lines of Terraform and a worse blast radius.

The visit counter does not store IP addresses

Deduplication needs to recognise a repeat visitor without retaining anything identifying. The key is a SHA-256 of the IP address, user agent, and date, salted with a value generated at deploy time and truncated to 128 bits. The raw address is never written, digests do not correlate across deployments, and the rows expire after 48 hours via DynamoDB TTL.

The conditional write that claims that key is also the concurrency control: two simultaneous requests race, one wins, and the counter moves exactly once.

Polling Cost Explorer per page view would cost more than the site

Cost Explorer bills $0.01 per API request. At even modest traffic, querying it on page load would dominate the bill it was reporting. The collector runs once a day and the API serves the cached snapshot from DynamoDB, which is why the cost panel updates daily rather than live — a deliberate tradeoff, not a limitation.

Terraform state locks on S3, not DynamoDB

S3 conditional writes have handled state locking since Terraform 1.11, and the DynamoDB mechanism is deprecated. One fewer resource to provision and pay for.

Security posture

  • Private origins; no public S3 URL and no public function URL.
  • TLS 1.2 minimum, HTTP to HTTPS redirect, HSTS with preload, and bucket policies that deny non-TLS requests outright.
  • Content-Security-Policy, X-Content-Type-Options, frame-ancestors 'none', and a restrictive Permissions-Policy, applied at the edge by a response headers policy.
  • Terraform state is versioned and encrypted, with public access blocked and deletion protection on.
  • Point-in-time recovery on the metrics table; a budget alarm at 80% of actual and 100% of forecast spend.

What it costs

Cost data is collected daily and will appear here once the first snapshot has run.

The fixed costs are the Route53 hosted zone at $0.50/month and the Cost Explorer requests at roughly $0.60/month. Everything else — CloudFront, S3, Lambda, DynamoDB — lands in the cents at personal-site traffic.

Reporting the cost of the reporting is the point. A system that cannot tell you what it costs is a system nobody is really operating.

Read the code

Nothing on this page needs to be taken on trust.