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
Step 1, Delivery
GitHub Actions
Build, sync, invalidate
Step 2, Data
S3 bucket
Private, Origin Access Control only
Step 3, Delivery
CloudFront + ACM
Global CDN over HTTPS
Step 4, Delivery
Route 53
Apex and www aliases
Step 5, Compute
Visitor counter
API Gateway → Lambda → DynamoDB
Engineering decisions
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.
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.
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.
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
Hosting
Backend
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.