Host a Static Website on Amazon S3

Host a portfolio site on S3 and serve it globally over HTTPS with CloudFront.

Introduction

30 Second Summary

Every website needs a home. Whether it's a portfolio, a landing page, or a documentation site, the files need to live somewhere that people around the world can access.

In this project, you will host a static website on Amazon S3 and serve it globally through CloudFront. Along the way, you'll write bucket policies, configure CORS, and set up Origin Access Control. These are all frequently tested on the AWS SAA-C03 exam.

What You'll Build

A two-page portfolio website that anyone in the world can open in their browser. You'll serve it first over HTTP from S3's website endpoint, then upgrade it to HTTPS delivery through CloudFront's global edge network.

By the end of this project, you'll have:

  • A live website URL you can share with anyone, served directly from your S3 bucket's website endpoint and visible in your browser.
  • An HTTPS distribution from CloudFront that delivers your site with global edge caching, with all direct S3 access locked down through Origin Access Control (OAC).
  • A working CORS configuration that allows JavaScript on a different domain to make requests to your bucket without the browser blocking them.
  • Secret Mission: Configure a CloudFront custom error response that serves a friendly page for any URL path, enabling single-page app routing.

Are there any prerequisites?

You'll need an AWS account with console access. No prior S3 or CloudFront experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Create an S3 Bucket and Upload Website Files

Every website needs a home. On Amazon S3, that home is a bucket filled with your website files.

In this step, you will create an S3 bucket and upload two HTML files to it. This teaches you about S3's object model, where every file becomes an object with a key (its name) and metadata (information about the file).

In this step, get ready to:
  • Create an S3 bucket with a globally unique name.
  • Write two HTML files on your PC (a portfolio page and a custom error page).
  • Upload both files to your bucket as S3 objects.
Create a new S3 bucket

An S3 bucket is a container that holds your files (called objects). Before you can store anything in S3, you need to create a bucket first.

  • Open your browser and navigate to the S3 console at https://console.aws.amazon.com/s3/.
  • Sign in with your AWS account credentials if prompted.
  • In the left navigation pane, choose General purpose buckets.
  • Choose Create bucket.

The Create bucket page opens with several configuration options.

  • For Bucket name, enter a globally unique name such as my-portfolio-site-2026.
  • For AWS Region, choose a Region close to you (for example, US East (N. Virginia) us-east-1).
  • Leave all other settings at their defaults. Block Public Access should remain ON and ACLs should remain disabled.
  • Scroll to the bottom and choose Create bucket.

Your new bucket now appears in the buckets list.

Why does the bucket name need to be globally unique?

S3 bucket names share a global namespace across all AWS customers. Your bucket name becomes part of the URL used to access your files, so no two buckets anywhere in the world can have the same name.

The name must be 3 to 63 characters long, use only lowercase letters, numbers, and hyphens, and start and end with a letter or number.

What is Block Public Access?

Block Public Access is a safety feature that prevents accidental public exposure of your bucket's contents. It overrides any bucket policy or ACL that would grant public access.

You will turn it off intentionally in a later step when you are ready to serve your website.

Bucket name already taken?

If you see an error saying the bucket name already exists, try adding random numbers or your initials to make it unique. For example, my-portfolio-jd-2026.

Help me troubleshoot this

Create and upload website files

Now you need content to put in your bucket. You will create two HTML files: a portfolio homepage and a custom error page for when visitors request a page that does not exist.

  • Press the Windows key to open your search bar.
  • Type Notepad and press Enter to open it.
  • Copy the following HTML into the empty Notepad window:
<!DOCTYPE html>
<html>
<head>
    <title>My Portfolio</title>
</head>
<body>
    <h1>Welcome to My Portfolio</h1>
    <p>This website is hosted on Amazon S3.</p>
    <p>I am learning cloud computing with NextWork.</p>
</body>
</html>

What does this HTML do?

This is a minimal HTML page with a heading and two paragraphs. It will serve as your portfolio homepage. S3 will return this file whenever someone visits the root of your website.

  • In Notepad, click File, then Save As.
  • In the Save as type dropdown, select All Files (*.*).
  • Name the file index.html and save it to your Desktop.

File saved as index.html.txt?

If you skip the All Files (*.*) step, Notepad adds a hidden .txt extension. Check your Desktop. If the file shows as index.html.txt, delete it and save again with the correct Save as type selected.

Help me fix the file extension

Now create the error page that S3 will return when a visitor requests a page that does not exist.

  • In Notepad, click File, then New to open a blank document.
  • Copy the following HTML into the empty document:
<!DOCTYPE html>
<html>
<head>
    <title>Page Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p>Sorry, the page you requested does not exist.</p>
    <p><a href="index.html">Return to homepage</a></p>
</body>
</html>

What does this HTML do?

This is a custom 404 error page. When S3 receives a request for a file that does not exist in your bucket, it can return this page instead of a generic error. The link at the bottom sends visitors back to your homepage.

  • Click File, then Save As.
  • Set Save as type to All Files (*.*) again.
  • Name the file error.html and save it to your Desktop.

You should now see both index.html and error.html on your Desktop.

Now upload both files to your S3 bucket.

  • Return to the S3 console in your browser.
  • In the buckets list, click the name of the bucket you just created.
  • On the Objects tab, choose Upload.
  • Choose Add files.
  • Navigate to your Desktop, select both index.html and error.html, then choose Open.
  • Choose Upload.

You will see a green success banner confirming both files uploaded successfully.

What are objects and keys in S3?

Everything you store in S3 is called an object. Each object has a key, which is its full name (including any folder prefixes). In your case, the keys are simply index.html and error.html because you uploaded them to the bucket root with no folder prefix.

Each object also has metadata. S3 automatically sets metadata like the content type and file size when you upload.

Before you move on, what do you expect to see when you click your bucket name and look at the Objects tab?

  • Choose Close on the upload success page to return to your bucket.
  • Confirm you see index.html and error.html listed under the Objects tab.

Both files are now stored as objects in your bucket. You can see their names (keys), sizes, and last modified timestamps.

Don't see both files?

Make sure you selected both files during the upload step. If only one appears, choose Upload again, add the missing file, and choose Upload.

Help me find my uploaded files

Your bucket now has content. Next up, you will enable static website hosting so S3 can serve these files as a real website anyone can visit in their browser.

Enable Static Website Hosting and Make the Site Public

Your bucket has files in it, but right now it is just a storage container. To turn it into a website, you need to enable S3's static website hosting feature.

Static website hosting gives your bucket a dedicated website endpoint that automatically serves index.html when someone visits the root URL. Without it, you would have to specify the full object key in every request.

In this step, get ready to:
  • Enable static website hosting on your bucket and get a website endpoint URL.
  • Make your bucket publicly accessible so the website actually loads.
Enable static website hosting
  • In your bucket, choose the Properties tab.
  • Scroll down to the Static website hosting section.
  • Choose Edit.
  • Under Static website hosting, select Enable.
  • In the Index document field, enter index.html.
  • In the Error document field, enter error.html.
  • Choose Save changes.

After saving, you will see a new Endpoint URL displayed under the Static website hosting section. Copy this URL.

What is a website endpoint?

S3 has two types of endpoints for every bucket. The REST endpoint (used for API calls) requires you to specify the full object key in the URL.

The website endpoint automatically serves index.html for root requests and returns your custom error page for missing objects. This distinction is a common SAA-C03 exam question.

Before you open this URL, what do you think will happen? The bucket still has Block Public Access turned on from when you created it.

  • Open a new browser tab and paste your website endpoint URL.

You see a 403 Forbidden error. This is expected and intentional.

Seeing 403 Forbidden? That is the plan

Even though you enabled website hosting, the bucket's Block Public Access setting prevents anyone from reading the objects. S3 applies the most restrictive combination of settings.

The SAA-C03 exam tests this relationship directly. Enabling static website hosting alone is not enough. You also need to disable Block Public Access AND add a bucket policy.

Disable Block Public Access and add a bucket policy

To make your site load, you need to do two things: turn off the Block Public Access guard, then tell S3 exactly who can read your objects using a bucket policy.

  • Choose the Permissions tab.
  • Under Block public access (bucket settings), choose Edit.
  • Clear the Block all public access checkbox.
  • Choose Save changes.
  • A confirmation dialog appears. Type confirm in the text field and choose Confirm.

The Block public access (bucket settings) section now shows all settings as Off.

Why does AWS ask you to type confirm?

Disabling Block Public Access means anyone on the internet could potentially read your bucket's objects. AWS adds this confirmation step to make sure you understand the security implications before proceeding.

  • Scroll down to the Bucket policy section.
  • Choose Edit.
  • Paste the following policy into the Bucket policy editor:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[[BUCKET_NAME="your-bucket-name"]]/*"
        }
    ]
}

What does this policy do?

  • Principal "*" means everyone in the world.
  • Action s3:GetObject means they can only read objects. They cannot upload, delete, or list the bucket.
  • Resource arn:aws:s3:::your-bucket-name/* targets every object inside your bucket.
  • This is the standard public-read policy structure you will see on the SAA-C03 exam.
  • Make sure you replace the bucket name in the Resource line with your actual bucket name.
  • Choose Save changes.

Seeing Policy has invalid resource?

This error means the bucket name in your policy's Resource field does not match your actual bucket name. Double check that the ARN reads arn:aws:s3::: followed by your exact bucket name, then /*.

If you get an error saying you cannot save the policy, check that Block Public Access is fully disabled. Help me fix my bucket policy error.

Now for the moment of truth. Do you think your website will load this time?

  • Switch back to your browser tab with the website endpoint URL.
  • Refresh the page.

Your portfolio page now loads successfully. You have a live website served directly from your S3 bucket.

Still seeing 403 Forbidden?

Make sure you cleared all four Block Public Access checkboxes, not just the top one. Also confirm your bucket policy was saved without errors.

If your browser shows a cached error, try opening the URL in an incognito window. Help me get my S3 static website to load.

Your website is live and publicly accessible over HTTP. Next, you will put CloudFront in front of it for HTTPS delivery and global edge caching.

Add CloudFront with Origin Access Control

Your portfolio site is live and publicly accessible. However, the S3 website endpoint only serves content over HTTP.

Production websites require HTTPS for encrypted connections. To get HTTPS in front of your S3 bucket, you need CloudFront. CloudFront also caches your content at edge locations around the world for faster delivery. In this step, you will lock down the bucket so only CloudFront can reach it, using Origin Access Control (OAC).

In this step, get ready to:
  • Remove public access from your S3 bucket.
  • Create a CloudFront distribution with OAC.
  • Verify your site loads over HTTPS through CloudFront.
Remove public access from the bucket

Before adding CloudFront, you need to undo the public access you granted in the previous step. Once CloudFront is in place, it will be the only path to your content.

  • Switch back to the S3 console tab with your bucket open.
  • Choose the Permissions tab.
  • Under Bucket policy, choose Edit.
  • Select all the policy text in the editor and delete it so the field is empty.
  • Choose Save changes.

Now re-enable Block Public Access to fully lock down the bucket.

  • Under Block public access (bucket settings), choose Edit.
  • Select Block all public access.
  • Choose Save changes.
  • Type confirm in the confirmation dialog, then choose Confirm.

Open your S3 website endpoint URL in a new browser tab. You should see a 403 Forbidden error. The bucket is locked down and no longer publicly accessible.

Why remove public access first?

With OAC, CloudFront signs every request it sends to S3 using the cloudfront.amazonaws.com service principal. If you left the public policy in place, anyone could still bypass CloudFront and hit S3 directly. Removing public access ensures the only way to reach your content is through CloudFront.

Create a CloudFront distribution with OAC

CloudFront is AWS's content delivery network (CDN). It caches your site at edge locations worldwide and serves it over HTTPS.

  • Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
  • Choose Create distribution.
  • Enter a Distribution name (for example, my-portfolio-distribution). This is just a label.
  • Choose Single website or app, then choose Next.

What about the Domain setup page?

You may see an optional Domain setup page. Skip it by choosing Next without entering a domain. You can add a custom domain later if you wish.

  • Choose Next to skip domain setup.
  • On the origin page, for Origin type, select Amazon S3.
  • For S3 origin, choose Browse S3 and select your my-portfolio-site-2026 bucket.
  • For Settings, choose Use recommended origin settings.

What does Use recommended origin settings do?

This option automatically creates an Origin Access Control (OAC) and configures it on your distribution. OAC is the recommended way to restrict your S3 bucket so only CloudFront can access it. It replaces the older Origin Access Identity (OAI) method.

CloudFront will also update your S3 bucket policy automatically to grant the cloudfront.amazonaws.com service principal read access.

  • Choose Next.
  • On the Enable security protections page, select Do not enable security protections (skip WAF for this project).
  • Choose Next.
  • Choose Create distribution.

CloudFront automatically updates your S3 bucket policy to grant access only through this distribution.

  • Review the Details section for your new distribution. Wait for the Last modified field to change from Deploying to a date and time. This typically takes 3-5 minutes.
  • Record the domain name CloudFront assigns to your distribution (it looks like d111111abcdef8.cloudfront.net).

Deployment takes a few minutes

CloudFront is deploying your content to edge locations around the world. Refresh the page periodically until the Last modified field shows a timestamp instead of Deploying.

Test HTTPS access

Your distribution is deployed. Before you open it, think about what you expect to see.

  • What do you think will happen when you visit your CloudFront domain name? Will your portfolio page load, or will you see an error?
  • Open a new browser tab and navigate to your CloudFront distribution domain name, using https:// at the start (for example, https://d111111abcdef8.cloudfront.net).

Your portfolio page loads over HTTPS. Notice the padlock icon in your browser's address bar confirming the secure connection.

  • Now try your S3 website endpoint URL again (the http:// one from Step 2).

It still returns 403 Forbidden. Direct access to S3 is blocked. The only way to reach your content now is through CloudFront.

Seeing an AccessDenied error on your CloudFront URL?

If you see an AccessDenied XML error at your CloudFront URL, the distribution may still be deploying. Wait until Last modified shows a timestamp, then try again.

If the error persists after deployment, check your S3 bucket's Permissions tab. Under Bucket policy, confirm a new policy exists that references cloudfront.amazonaws.com. CloudFront should have added this automatically.

Why is my CloudFront distribution showing AccessDenied?

Your site is now served securely over HTTPS through CloudFront, with OAC restricting all S3 access to CloudFront only. Next up, you will configure CORS on your S3 bucket to allow cross-origin requests from JavaScript.

Configure CORS on Your S3 Bucket

Your site is live over HTTPS through CloudFront, but there is one more access scenario the SAA-C03 exam loves to test.

When JavaScript running on one domain tries to fetch a resource from a different domain, the browser blocks the request unless the target server explicitly allows it. This mechanism is called CORS (Cross-Origin Resource Sharing). In this step, you will add a CORS configuration to your S3 bucket so that scripts served from your CloudFront domain can make requests directly to the bucket's REST endpoint.

In this step, get ready to:
  • Add a CORS JSON rule to your S3 bucket.
  • Test cross-origin requests from your browser's developer console.
  • Prove that removing CORS blocks the same request.
Add a CORS configuration

Your bucket's REST endpoint lives at a different domain than your CloudFront distribution. Without a CORS rule, a browser will refuse any JavaScript fetch() call from the CloudFront origin to the S3 REST endpoint.

  • Return to your S3 bucket in the console.
  • Choose the Permissions tab.
  • Scroll down to Cross-origin resource sharing (CORS) and choose Edit.
  • Paste the following JSON CORS rule into the editor:
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET"
        ],
        "AllowedOrigins": [
            "https://[[CLOUDFRONT_DOMAIN=\"your CloudFront distribution domain\"]]"
        ],
        "ExposeHeaders": []
    }
]

What does this CORS rule do?

This rule tells S3 to accept GET requests from your CloudFront domain.

  • AllowedOrigins specifies the exact origin (scheme + domain) that may send cross-origin requests. Your CloudFront URL is the only allowed origin here.
  • AllowedMethods restricts which HTTP verbs are permitted. We only need GET for reading objects.
  • AllowedHeaders set to * means any request header is acceptable in the preflight check.
  • Choose Save changes.

Your bucket now has a CORS configuration attached.

Test CORS behavior

The best way to see CORS in action is to make a cross-origin request from your browser and watch the headers that come back.

  • Open your CloudFront site in a new browser tab by navigating to your distribution domain (e.g. https://d111111abcdef8.cloudfront.net).
  • Open DevTools by pressing F12 (Windows) or Cmd+Option+I (macOS).
  • Click the Console tab.

Before you run the next command, think about what you expect to happen. Will the browser allow the request or block it?

  • Paste the following fetch() call into the Console and press Enter:
fetch('https://[[BUCKET_REST_ENDPOINT="my-portfolio-site-2026.s3.us-east-1.amazonaws.com"]]/index.html')
  .then(r => console.log('Status:', r.status, r.headers.get('Access-Control-Allow-Origin')))
  .catch(e => console.error('CORS blocked:', e))

What does this fetch do?

This fetch sends a GET request from your CloudFront page (one origin) to the S3 REST endpoint (a different origin). The browser performs a CORS check before letting your JavaScript see the response.

Because you added the CORS rule, S3 responds with an Access-Control-Allow-Origin header matching your CloudFront domain. The browser allows the response through.

You should see output like Status: 200 followed by your CloudFront domain printed as the allowed origin. The request succeeded.

Seeing a CORS error instead of 200?

  • Check that the AllowedOrigins value in your CORS config exactly matches the URL in your browser's address bar, including https:// and no trailing slash.
  • Confirm you saved the CORS configuration. Return to the Permissions tab and verify the JSON is still there.
  • If you copied the domain with a trailing slash, remove it and save again.

Why is my fetch() failing with a CORS error even though I added the rule?

Now let's prove that this works because of CORS. You will remove the config and watch the same request fail.

  • Go back to your bucket's Permissions tab.
  • Under Cross-origin resource sharing (CORS), choose Edit.
  • Delete all the JSON text so the editor is empty.
  • Choose Save changes.
  • Return to your CloudFront tab and open the Console in DevTools.
  • Run the same fetch() call again.

This time the Console prints a CORS error. Without the configuration, S3 does not return the Access-Control-Allow-Origin header, so the browser blocks the response. This is exactly the behavior the SAA-C03 exam tests.

Why does the exam care about this?

The SAA-C03 presents scenarios where JavaScript on a different domain needs to access objects in an S3 bucket and asks what configuration is needed. The answer is always a CORS configuration on the bucket. Bucket policies and ACLs control who can access objects. CORS controls whether a browser allows the response to reach your JavaScript.

  • Re-add the CORS configuration to restore the working state. Go back to Permissions > Cross-origin resource sharing (CORS) > Edit.
  • Paste the same JSON rule you used earlier (allowing GET from your CloudFront domain).
  • Choose Save changes.
  • Run the fetch() call one more time in the DevTools Console to confirm it succeeds again with Status: 200.

The request succeeds once more. Your CORS configuration is working and restored.

CORS is configured and tested. Next up, you will tackle the Secret Mission and configure CloudFront custom error responses for single-page app routing.

Secret mission

Configure CloudFront Custom Error Response for SPA Routing

Single-page applications need every route to return index.html so client-side routing works. Can you configure CloudFront to intercept S3's 403 errors and return your index page with a 200 status code instead?

Clean Up Your Resources

Clean Up Your Resources

This project uses minimal cloud resources. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Cost warning

Your S3 bucket holds only two small HTML files, which costs fractions of a penny per month ($0.023/GB/month for Standard storage). Your CloudFront distribution has very low cost for minimal usage. However, if you leave resources running indefinitely, charges will accrue. Consider pausing or deleting if you are done.

Resources you used:

  • S3 bucket (my-portfolio-site-2026) containing index.html and error.html.
  • CloudFront distribution (d111111abcdef8.cloudfront.net) with Origin Access Control (OAC).
  • Origin Access Control (OAC) attached to the CloudFront distribution.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend this project soon.

  • Your S3 bucket stores only two small files. Storage cost is negligible (fractions of a penny per month).
  • Your CloudFront distribution has very low cost for minimal usage.
  • The OAC remains attached and keeps your bucket secure.

Pause - I'll come back to this later

Disable the CloudFront distribution to stop serving traffic and prevent request charges. Your S3 bucket and files remain intact for when you return.

  • Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
  • Select the checkbox next to your distribution.
  • Choose Disable, then confirm with Yes, Disable.
  • Wait until the Last modified column shows a new timestamp. This may take a few minutes.
  • Your distribution is now paused. To resume later, select it and choose Enable.

Delete - I don't want to use this again

Remove all project resources completely. Follow these steps in order: delete the CloudFront distribution first, then the OAC, then empty and delete the S3 bucket.

Delete the CloudFront distribution
  • Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
  • Select the checkbox next to your distribution.
  • Choose Disable, then confirm with Yes, Disable. Choose Close.
  • Wait until the Last modified column shows a new timestamp. This may take a few minutes as CloudFront propagates to all edge locations.
  • Select the checkbox for the distribution again.
  • Choose Delete, then choose Delete to confirm.
Delete the Origin Access Control (OAC)
  • In the CloudFront console, choose Origin access in the left navigation pane.
  • Select the OAC you created for this project.
  • Choose Delete and confirm.
Empty and delete the S3 bucket
  • Open the S3 console at https://console.aws.amazon.com/s3/.
  • In the Buckets list, select my-portfolio-site-2026 and choose Empty.
  • Type permanently delete to confirm, then choose Empty.
  • Return to the Buckets list, select the bucket again, and choose Delete.
  • Type the bucket name to confirm, then choose Delete bucket.

That's a Wrap!

That's a Wrap!

Nice work! You just hosted a static website on Amazon S3 and served it globally over HTTPS with CloudFront.

You've learned how to:

  • Host a static website on S3, write a bucket policy for public access, then lock it down with Origin Access Control (OAC) so only CloudFront can reach the bucket.
  • Serve your site over HTTPS with a CloudFront distribution and understand the difference between S3 website endpoints (HTTP, returns index.html for root requests) and REST endpoints (HTTPS, requires the full object key).
  • Configure CORS on your S3 bucket to allow cross-origin JavaScript requests from a different domain.
  • Secret Mission: Configure CloudFront custom error responses to return index.html with a 200 status code for non-existent paths, enabling single-page app routing.

Ready to quiz yourself?