Skip to content
Cloud / DevOpsShippedJun 2026

Cloud Resume Challenge — AWS

This portfolio ran end-to-end on AWS: static export on S3, served via CloudFront with a custom domain on Route 53, and a serverless visitor counter on Lambda + API Gateway + DynamoDB — fully automated with a GitHub Actions deploy pipeline. The site has since moved to Vercel.

Role
Cloud Engineer (Self-directed)
Timeline
Jun 2026
Team
Solo
Status
Shipped

The problem

The Cloud Resume Challenge: take a personal site off managed hosting and ship it on a real cloud — wiring storage, CDN, DNS, compute, a database, and a deploy pipeline together yourself, with security and cost treated as requirements, not afterthoughts.

Constraints

  • No managed-hosting shortcuts — AWS primitives only
  • The bucket must never be publicly readable
  • CloudFront requires the TLS certificate to live in us-east-1
  • Least-privilege credentials for the CI pipeline
  • Near-zero running cost

Architecture

  1. Step 1, Delivery

    GitHub Actions

    Build, sync, invalidate

  2. Step 2, Data

    S3 bucket

    Private, Origin Access Control only

  3. Step 3, Delivery

    CloudFront + ACM

    Global CDN over HTTPS

  4. Step 4, Delivery

    Route 53

    Apex and www aliases

  5. Step 5, Compute

    Visitor counter

    API Gateway → Lambda → DynamoDB

A Next.js static export is synced to a private S3 bucket and served by CloudFront through Origin Access Control, with an ACM certificate and Route 53 aliases for the apex and www. A visitor counter runs on API Gateway, a Node.js Lambda, and an atomic DynamoDB counter. GitHub Actions builds, syncs, and invalidates on every push.

Engineering decisions

  1. A private bucket behind Origin Access Control

    Context
    A public S3 website endpoint would expose the bucket directly and let traffic skip the CDN.
    Decision
    Kept the bucket private and let only CloudFront read it, through an Origin Access Control policy.
    Trade-off
    The REST origin has no index documents, so directory URLs need a rewrite at the edge.
  2. An atomic counter, not read-modify-write

    Context
    Concurrent visits could race if the Lambda read the count, added one, and wrote it back.
    Decision
    A single DynamoDB UpdateItem with ADD increments the counter atomically, on on-demand billing.
    Trade-off
    One hot item — more than enough for a portfolio's traffic.
  3. Split cache headers

    Context
    Hashed assets never change, but HTML has to reflect each deploy immediately.
    Decision
    Synced hashed assets with immutable, year-long caching and HTML with no-cache, then invalidated CloudFront.
  4. Least-privilege deploy credentials

    Context
    Most of the friction in the whole project turned out to be IAM.
    Decision
    Gave the pipeline a dedicated identity scoped to this one bucket and this one distribution.

Outcomes

S3 + CloudFront (global CDN)

Hosting

Serverless (Lambda + DynamoDB)

Backend

GitHub Actions CI/CD

Deploy

  • From git push to live in under five minutes
  • Monthly running cost within a couple of dollars, domain aside
  • A serverless visitor counter with no race conditions and no idle cost

Reflections

Why AWS

I picked AWS because the underlying primitives (S3, CloudFront, Route 53, Lambda, DynamoDB, API Gateway, IAM) are the same vocabulary used across most production systems I want to work on, and forcing myself to use the console first meant I had to actually understand the moving parts before hiding them behind Terraform.

Lessons learned

The biggest lesson was how much of "AWS expertise" is really IAM expertise: 80% of the friction was getting policies right (OAC bucket policies, Lambda execution roles, GitHub Actions least privilege), and once those clicked, the rest of the services felt like Lego.

Proof — verify it yourself

The source and demo for this project aren’t public. Get in touch for a walkthrough.

Tech stack

  • AWS S3
  • CloudFront
  • Route 53
  • AWS Lambda
  • DynamoDB
  • API Gateway
  • GitHub Actions
  • Next.js

Related writing

  • Guide

    A static site on S3 and CloudFront, without the footguns

    The decisions behind this site's original AWS setup — a private bucket, directory URLs at the edge, split cache headers, an atomic visitor counter — and why IAM took the most time.

    3 min read

    • #aws
    • #cloudfront
    • #devops
    • #nextjs

Work with me

Facing a similar challenge?

I build systems like this end to end — from the first architecture sketch to production. Tell me about yours.