I Built and Deployed My First Portfolio on AWS S3 + CloudFront — And Broke It Along the Way
As an M.Sc Artificial Intelligence and Machine Learning student, I've been trying to move away from just writing code by following tutorials and actually understand how things work by building and deploying projects.
One of the things I wanted to build was a simple personal portfolio.
It sounded easy:
Build a portfolio → upload it to AWS → get a public URL.
It wasn't quite that simple.
I ran into permissions issues, S3 configuration problems, CloudFront errors, and eventually discovered that I had accidentally configured CloudFront to look for:
index.html/index.html
This is the story of how I built it, broke it, debugged it, and finally understood what was actually happening.
1. Starting With a Simple HTML Portfolio
I initially created my portfolio using a single:
index.html
I kept the design intentionally simple.
The portfolio contained:
- Experience
- Selected Work
- Skills
- Education
- Contact
Some of the projects I included were:
- BloodBridge — intelligent blood donor matching platform
- CITYMIND — a digital twin for city decisions
- IPL Match Winner Prediction
- Rural Water Monitoring and Transparency System
- Textile Hub — a B2B network for the textile trade
At this point, everything worked locally.
I could simply open the HTML file in my browser and see the portfolio.
But then I wanted to actually deploy it.
2. Moving to AWS S3
I decided to use Amazon S3 for hosting.
I created an S3 bucket and uploaded my portfolio.
The object looked like:
s3://portfolio-prasanth/index.html
My first assumption was:
"The file is uploaded, so anyone should be able to open it."
That assumption was wrong.
When I tried accessing the website, I received:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
</Error>
3. First Struggle — S3 AccessDenied
This was my first real AWS deployment problem.
The HTML itself wasn't the issue.
The problem was with S3 permissions and website access configuration.
I had to understand the relationship between:
S3 Bucket
↓
Object
↓
Permissions
↓
Static Website Hosting
I configured static website hosting and worked through the public-access configuration and bucket policy.
The important part of the bucket policy was allowing public GetObject access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::portfolio-prasanth/*"
}
]
}
But then I ran into another issue.
4. My Bucket Name Wasn't What I Thought
At one point I was working with:
prasanth-portfolio-bucket
But when I looked at my AWS console, the actual bucket I was configuring was:
portfolio-prasanth
That caused confusion while creating the bucket policy.
AWS was telling me:
Policy has invalid resource
The reason was simple.
The policy resource has to reference the actual bucket name.
So:
arn:aws:s3:::portfolio-prasanth/*
instead of:
arn:aws:s3:::prasanth-portfolio-bucket/*
That was a small mistake, but it taught me something important:
Don't blindly copy AWS configuration. Understand what resource you're actually configuring.
5. Understanding the Static Portfolio Structure
Since my portfolio is a single self-contained index.html — HTML, CSS, and JavaScript all in one file, with a small canvas animation in the hero section — there was no build step involved. No npm run build, no dist/ folder, no bundler.
That turned out to be both a simplification and a lesson in itself:
index.html
↓
AWS S3
The browser doesn't need a server running to interpret my markup — it just needs a place that can serve the file over HTTP. S3 static website hosting does exactly that.
6. Adding CloudFront
Then I wanted to put CloudFront in front of S3, mainly to get HTTPS and a CDN in front of the site rather than hitting the S3 website endpoint directly.
The architecture became:
┌──────────────┐
│ Browser │
└──────┬───────┘
│
▼
┌──────────────┐
│ CloudFront │
└──────┬───────┘
│
▼
┌──────────────┐
│ S3 │
│ index.html │
└──────────────┘
This was the point where things got interesting.
7. The CloudFront 404 Error
Instead of my portfolio, CloudFront returned:
404 Not Found
Code: NoSuchKey
Message: The specified key does not exist.
Key: index.html/index.html
At first I had no idea why AWS was asking for:
index.html/index.html
My S3 bucket clearly contained:
index.html
not:
index.html/index.html
So I started looking at the CloudFront configuration.
8. Finding the Actual Problem
I checked the CloudFront origin configuration.
And there it was.
My origin looked like:
Origin type: S3 static website
Origin path: /index.html
That was the mistake.
CloudFront already knew that the default root object was:
index.html
And I had also told the origin to use:
/index.html
So CloudFront effectively ended up requesting:
/index.html/index.html
S3 obviously couldn't find that object.
The error suddenly made sense.
9. The Fix
I changed the CloudFront origin path from:
/index.html
to:
(empty)
And kept the CloudFront default root object as:
index.html
So the configuration became conceptually:
CloudFront
Default root object:
index.html
Origin:
portfolio-prasanth S3 website endpoint
Origin path:
(empty)
Now the request flow made sense:
/
↓
CloudFront
↓
index.html
↓
S3
↓
portfolio
instead of:
/
↓
CloudFront
↓
index.html
↓
/index.html
↓
index.html/index.html ❌
10. Why I Had to Invalidate CloudFront
Even after fixing the configuration, CloudFront can still have cached responses.
So I created a CloudFront invalidation:
/*
This tells CloudFront to remove the cached objects so that the next request can fetch the updated content.
11. What I Actually Learned
The biggest thing I learned from this wasn't HTML or CSS.
It wasn't even CloudFront.
It was debugging.
Before this, when something didn't work, my first instinct was often:
"The code must be wrong."
But this project showed me that problems can exist at completely different layers.
For example:
Frontend
↓
Static files
↓
S3
↓
Permissions
↓
CloudFront
↓
Caching
↓
Browser
A website can have perfectly valid markup and still return a 404 because the infrastructure configuration is wrong.
12. My Struggles
These were some of the problems I faced:
S3
AccessDenied
I had to understand bucket permissions and public access.
Bucket policy
Policy has invalid resource
I was referencing the wrong bucket name.
CloudFront
NoSuchKey
Key: index.html/index.html
I had incorrectly configured the origin path.
Account verification
Before I could even create a CloudFront distribution, AWS returned:
Your account must be verified before you can add new CloudFront resources.
New AWS accounts sometimes need manual verification through AWS Support before CloudFront resources can be created — a step I hadn't expected as part of "just hosting a webpage."
Caching
Even after fixing the configuration, CloudFront could still serve an old cached response.
13. What I Would Do Differently Next Time
If I were starting this deployment again, my process would be:
1. Write and test the portfolio locally
↓
2. Upload index.html to S3
↓
3. Configure S3 static website hosting + bucket policy
↓
4. Verify AWS account (if needed) before touching CloudFront
↓
5. Configure CloudFront origin correctly the first time
↓
6. Test CloudFront
↓
7. Check logs/errors if something fails
↓
8. Invalidate cache when required
And most importantly:
I would change one configuration at a time and test after each change.
14. Final Architecture
The final concept I learned is:
┌───────────────────┐
│ User │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ CloudFront │
│ CDN │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ S3 Bucket │
│ │
│ index.html │
└───────────────────┘
My portfolio is a single static file, served through AWS infrastructure and a CDN in front of it.
Final Thoughts
This was a relatively small project, but it gave me something more valuable than just a portfolio.
It gave me experience with the uncomfortable part of development:
things not working.
I had to read errors, inspect configuration, understand what AWS was actually doing, find the mismatch, and fix it.
The index.html/index.html error in particular was frustrating at first.
But once I understood why it happened, it became one of the most useful lessons from the project.
I'm still learning AWS, machine learning, and software development, but I'm slowly trying to replace:
"Copy the config and hope it works"
with:
"Understand what each component is doing, then debug when it doesn't."
And I think that's a much better way to learn.
Tech Stack
Frontend
- HTML
- CSS
- JavaScript
Cloud
- Amazon S3
- Amazon CloudFront
Background
- Python, PyTorch, TensorFlow, Scikit-learn
- React, TypeScript, Supabase
Development
- VS Code
- Git/GitHub




Top comments (0)