
Weekend Deployment Challenge: Portfolio Site on S3 + CloudFront
A serverless personal portfolio hosted on a private S3 bucket and served over HTTPS through CloudFront — one CloudFormation template, zero servers, and effectively zero cost on the AWS Free Tier.
TL;DR / Key Takeaways
- Serverless & Secure by Default: The site runs with no web server at all — a private S3 bucket fronted by CloudFront, with the bucket never exposed to the public internet.
- Infrastructure as Code: The entire stack (S3, CloudFront, OAC, bucket policy) is defined in a single CloudFormation template, so the whole thing deploys — or tears down — with one command.
- Zero Ongoing Costs: Designed to fit comfortably within the AWS Free Tier, making it effectively free to run indefinitely.
Introduction
Hey everyone, what's up? Every developer eventually needs a portfolio site, and every developer eventually over-thinks it. I have watched people spin up a full server for a single static page, pay for hosting they will never fully use, or hand their content over to a page builder they do not control. For this weekend's challenge I wanted to do the opposite: own the content, own the infrastructure, keep it fast and secure, and pay effectively nothing to keep it online.
The result is a single-page portfolio served over HTTPS from a private Amazon S3 bucket through an Amazon CloudFront distribution — with the entire infrastructure captured in one CloudFormation template. This is my submission for the #deployment tag. In this post, I am going to break down exactly what the site does, how I built it, the AWS services I glued together, and the lessons I picked up along the way.
Let's get into it.
Table of Contents
- Vision and What It Does
- Why I Built This (The Impact)
- How I Built It: Architecture & Challenges
- The AWS Services Used
- Key Decisions & The Solution
- What I Learned
- Screenshots
- Frequently Asked Questions
- Conclusion
Vision and What It Does
The core vision was simple: a personal site should be something you can hand someone a link to, not a system you have to babysit. The application is a single-page portfolio — a hero section with my name and a one-line professional summary, an about section, a skills list, a grid of my projects, and a contact section that links to my LinkedIn. It is fully responsive across mobile, tablet, and desktop, and it loads near-instantly.
Here is what makes it interesting: there is no web server in the request path at all. A visitor opens the URL, the page loads over HTTPS, they scroll through the work, and they click through to external links. Behind that URL, a CloudFront edge location serves the content and — on a cache miss — retrieves a single static file from a private storage bucket. The user-facing simplicity is the whole point; the engineering is in making that simplicity secure and inexpensive.
Why I Built This (The Impact)
Honestly speaking, hosting a simple personal site is one of the most over-engineered problems in tech. People provision an EC2 instance for a static page, then spend the next year patching, monitoring, and paying for it. I wanted maximum impact with zero maintenance.
"The best personal site is one you can deploy in a single command, forget about, and never pay for."
By designing this strictly as a serverless static-hosting stack, I kept the running cost at effectively $0. CloudFront's Free Tier covers the bandwidth, S3 storage for a single HTML file is negligible, and there is no compute in the path to bill for. It proves that you don't need a server — or a budget — to run a fast, secure, production-grade website if you architect it smartly.
How I Built It: Architecture & Challenges
Building this wasn't about writing a web server; it was about properly wiring up the AWS ecosystem using Infrastructure as Code (CloudFormation). The design followed one non-negotiable principle I set for myself: the S3 bucket is never public.
The quickest path — enabling S3 static website hosting and making the bucket world-readable — works, but it is the wrong pattern. It forfeits HTTPS, exposes the bucket directly, and gives up CDN caching and compression. Instead, I used the production-grade approach: a private S3 bucket with all public access blocked, fronted by CloudFront, connected through Origin Access Control (OAC).
1
2
3
4
┌─────────────┐ ┌──────────────────┐ ┌────────────┐
│ Browser │──HTTPS──▶ CloudFront CDN │──OAC───▶ S3 Bucket │
│ (Visitor) │◀────────│ (Distribution) │◀───────│ (Private) │
└─────────────┘ └──────────────────┘ └────────────┘The visitor communicates only with CloudFront. CloudFront serves cached content at the edge and, on a miss, fetches from S3 using its signed OAC identity. S3 accepts only that signed request from this specific distribution. There is no origin server, no compute, and no publicly accessible bucket anywhere in the path.
The AWS Services Used
The stack intentionally uses a minimal set of services:
- Storage (Amazon S3): Stores the static site (
index.html). Private bucket, all public access blocked, server-side encryption (SSE-S3) enabled by default. - CDN (Amazon CloudFront): CDN and TLS termination. Provides caching, gzip/Brotli compression, HTTP/2 and HTTP/3, and HTTPS redirection.
- Security (Origin Access Control — OAC): The secure link between CloudFront and S3, scoped to a single distribution via an
AWS:SourceArncondition. - IaC (AWS CloudFormation): Provisions and tears down the entire stack from one
template.yaml.
Key Decisions & Challenges
OAC over a public bucket. OAC is the component that makes this secure. CloudFront signs its origin requests to S3 using SigV4, and the bucket policy grants
s3:GetObject only to the CloudFront service principal for this distribution. As a result, no other distribution — and no anonymous request from the internet — can read the bucket. CloudFront is the only path to the content.Cost and performance tuning. I chose the distribution settings deliberately:
PriceClass_100 to limit delivery to the least expensive edge locations, Compress: true, HTTP/2 and HTTP/3 enabled, and redirect-to-https so all traffic is upgraded to TLS. I added custom 403/404 error responses that return index.html, which keeps deep links and refreshes working like a single-page app.No framework. I kept the front end as a single
index.html file with inline CSS and a tiny bit of JavaScript (only to render the current year in the footer). For a static portfolio, a build step adds complexity without benefit, and a self-contained file has no supply-chain surface.Now for the challenges I actually hit:
AccessDeniedon first load. My first request returned an XMLAccessDeniedfrom S3 — a common outcome with private buckets, with two distinct causes. Either you requested the raw S3 URL instead of the CloudFront domain (direct access is correctly refused), or you opened the CloudFront URL beforeindex.htmlwas uploaded (with onlys3:GetObjectgranted and no object present, S3 returnsAccessDenied, not 404). The fix for both is the same disciplined sequence: deploy → read theCloudFrontDomainNameoutput → upload the file → open that URL.- Stale cache after updates. After editing and re-uploading, CloudFront kept serving the old version. The fix is a
create-invalidationon/*after each upload. I documented this in the README so I wouldn't rediscover it later. - Teardown ordering. CloudFormation can't delete a non-empty S3 bucket, so cleanup must empty the bucket first, then delete the stack. I captured this as an explicit, sequential set of commands.
With those addressed, the deployment loop became straightforward: edit HTML →
aws s3 cp → invalidate → refresh.What I Learned
My most valuable takeaway was a concrete understanding of the private-bucket-with-OAC pattern rather than treating it as boilerplate. OAC supersedes the older Origin Access Identity mechanism, and once I recognized that the security boundary is a SigV4-signed request pinned to a distribution ARN, the whole design became easy to reason about.
I also sharpened how I interpret AWS error responses. An S3
AccessDenied does not necessarily mean a permissions bug; with a private bucket it frequently means my request went to the wrong endpoint or the object doesn't exist yet. Distinguishing those cases quickly eliminated a whole class of debugging guesswork.Operationally, treating the site as infrastructure as code changed how I work. Deploy, invalidate, and — importantly — tear down are each a single command. Clean teardown means no orphaned resources quietly accruing cost, which matters especially on the Free Tier. I found teardown discipline to be as important as the deployment itself.
Finally, the build reinforced my belief that "no framework" is a legitimate engineering decision. A single self-contained HTML file loads faster, deploys trivially, and carries no dependency risk — exactly the right fit for a static portfolio.
Screenshots









Frequently Asked Questions
Is this project really free to run?
Yes, it is effectively $0. CloudFront's Free Tier covers 1TB/month of bandwidth, S3 storage for a single HTML file is negligible, and there is no compute in the request path to bill for. As long as you stay within normal usage, AWS won't charge you.
Why not just make the S3 bucket public?
A public bucket forfeits HTTPS, exposes the bucket directly, and gives up CDN caching and compression. Keeping the bucket private and serving through CloudFront with OAC gives you HTTPS, edge caching, compression, and a much smaller attack surface — the production-grade pattern.
Why did I get an
AccessDenied error the first time? Because the bucket is private by design. Either you opened the raw S3 URL instead of the CloudFront domain, or you opened the CloudFront URL before uploading
index.html. Deploy the stack, upload the file, then open the CloudFrontDomainName output.How do I update the site after deploying?
Edit
index.html, run aws s3 cp to re-upload it, then create a CloudFront invalidation on /* so the CDN serves the fresh version instead of a cached copy.Conclusion
Building this serverless portfolio has been one of the most satisfying weekend challenges I have taken on. I went from a simple idea — a personal site I never have to babysit — to a fully serverless, cost-free, secure infrastructure using modern cloud architecture.
It proves that when you piece together small, well-scoped services, you can build fast, robust, production-grade applications without breaking a sweat (or your bank account). Keep shipping!
Link to the App/Repo: Check out the source code and CloudFormation template in my repository: github.com/ImSaMPro/AWS-Weekend-Agent-Challenge (see the
Portfolio-Site/ folder).Call to Action
Ready to host your own site the serverless way? Drop a comment below or check out the GitHub Repo to deploy this stack yourself!
Author Bio: Soumyadeep is a cloud enthusiast and builder who loves turning caffeine into serverless architecture. When he is not writing CloudFormation templates, he is probably trying to optimize his AWS billing dashboard.
Related Reading:
- Weekend Showcase Challenge: Daily Haiku Gallery
- Weekend Creative Agent Challenge: Daily Haiku Agent
- Weekend Creative Challenge: Daily Color Palette Extractor
- Building Cloud Skills in the City of Joy: My Life as an AWS Community Builder
- Weekend Annoying Task Challenge: Serverless PingGuard Uptime Monitor
- Weekend Agent Challenge: Morning Brief Agent | Build on AWS
- How I Built an Auto-Changelog Generator for the Kiro Birthday Challenge 2026
- Weekend Productivity Challenge: AI Task Prioritizer
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article