Deliver a Static Website with CloudFront
Serve a private S3 website globally using CloudFront with Origin Access Control.
Introduction
30 Second Summary
When you visit a website, your request might travel thousands of miles to a single server sitting in one region. Content Delivery Networks solve this by caching copies of your content at hundreds of locations worldwide, so the nearest one always responds.
In this project, you will deploy a static website through Amazon CloudFront, AWS's content delivery network with 750+ edge locations. You'll secure your S3 bucket so only CloudFront can serve the content, experience the stale-cache problem firsthand, and fix it with a cache invalidation.
What You'll Build
You'll have a live website URL that loads your pages from whichever CloudFront edge location is closest to the visitor, while the S3 bucket behind it remains completely locked down and inaccessible to anyone trying to reach it directly.
By the end of this project, you'll have:
- A live CloudFront URL you can visit in any browser and see your website load instantly from the nearest edge location.
- A private S3 bucket that rejects all direct access with "Access Denied" while the same content loads perfectly through CloudFront, secured with Origin Access Control.
- First-hand experience with the caching lifecycle: you'll watch cache hits in your browser's developer tools, hit the stale content problem after updating a file, and fix it by running a cache invalidation.
- Secret Mission: Create a custom cache behavior with aggressive TTL settings to serve images with a different caching strategy than your HTML pages.
Are there any prerequisites?
You'll need an AWS account and basic familiarity with Amazon S3 (creating buckets and uploading files). No CLI tools or local software are required. This project runs entirely in the AWS Management Console.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters for your AWS certification journey.
Set Up Your S3 Origin
CloudFront delivers content from 750+ edge locations worldwide, but it needs a source to pull that content from. That source is called an origin.
In this step, you will create a private S3 bucket and upload a simple website to it. Keeping the bucket fully private is critical because in the next step, you will prove that only CloudFront can serve this content to the world.
In this step, get ready to:
- Create an S3 bucket with Block Public Access fully enabled.
- Upload a multi-page static website to your bucket.
- Confirm the bucket is truly private by testing direct access.
Create your S3 bucket
You need a bucket to store your website files. This bucket will act as the origin for your CloudFront distribution.
- In the AWS Management Console, use the search bar at the top to search for S3 and open the S3 console.
- In the left navigation pane, choose General purpose buckets.
- Click Create bucket.
- In the Bucket name field, enter a globally unique name like my-cloudfront-website- followed by your name or a random string (e.g., my-cloudfront-website-jane2026).
- Confirm the AWS Region is set to your preferred region (e.g., us-east-1).
- Scroll down to Block Public Access settings for this bucket. Confirm all four checkboxes are enabled (they are on by default).
- Scroll to the bottom and click Create bucket.
You should see a green success banner and your new bucket in the buckets list.
Why keep Block Public Access on?
Block Public Access prevents anyone from directly downloading your files via the S3 URL. In Step 2, CloudFront will use Origin Access Control (OAC) to securely reach your bucket on the learner's behalf. This pattern is a core SAA-C03 concept: the bucket stays locked, and only the CDN can serve its content.
Bucket creation failed?
If you see a naming error, your bucket name may already be taken globally. Try adding more random characters to make it unique.
If you see a permissions error, make sure you are signed in with a user or role that has the s3:CreateBucket permission.
Still stuck? Help me fix my S3 bucket creation error.
Create and upload your website files
Your bucket needs website content that you can later view through CloudFront. You will create two HTML files and upload them along with an image.
- On your computer, open a text editor by pressing Cmd+Space (macOS) or the Windows key (Windows) and searching for Notepad (Windows) or TextEdit (macOS).
- Create a new file, paste the following HTML content, and save it to your Desktop as index.html:
<!DOCTYPE html>
<html>
<head>
<title>My CloudFront Website</title>
</head>
<body>
<!-- Main heading visitors will see -->
<h1>Welcome to My CloudFront Website</h1>
<p>This page is delivered through Amazon CloudFront.</p>
<!-- Image from the images folder -->
<img src="images/hero.jpg" alt="Hero image" width="600">
</body>
</html>
What does this file do?
This is a simple homepage with a heading, a paragraph, and a reference to an image in the images/ folder. You will use this page to verify CloudFront is working in the next step.
- Create a second file in your text editor, paste the following HTML, and save it to your Desktop as error.html:
<!DOCTYPE html>
<html>
<head>
<title>Error - Page Not Found</title>
</head>
<body>
<!-- Friendly error message -->
<h1>Oops! Page Not Found</h1>
<p>The page you requested does not exist.</p>
</body>
</html>
What is error.html for?
CloudFront can serve a custom error page when a visitor requests a file that does not exist. You will configure this in a later step.
- Find any image on your computer (a photo, a screenshot, or any .jpg or .png file) and rename it to hero.jpg. Save it on your Desktop alongside the HTML files.
You should now have three files on your Desktop: index.html, error.html, and hero.jpg.
macOS TextEdit users
TextEdit defaults to rich text mode. Before pasting, go to Format in the menu bar and choose Make Plain Text. Otherwise your file will be saved as .rtf instead of .html.
Now upload everything to your S3 bucket.
- In the S3 console, click the name of the bucket you just created.
- On the Objects tab, click Upload.
- Click Add files, select index.html and error.html from your Desktop, and click Open.
- Click Upload at the bottom of the page to upload both files.
- Click Close to return to the bucket.
You should see index.html and error.html listed in the Objects tab.
- Now create the images folder. On the Objects tab, click Create folder.
- Enter images as the folder name and click Create folder.
- Click the images/ folder to open it.
- Click Upload, then Add files, select your hero.jpg file, and click Open.
- Click Upload to upload the image, then click Close.
Your bucket now contains index.html, error.html, and an images/ folder with hero.jpg inside it.
Upload failed?
Make sure you selected the correct files from your Desktop. If you see a permissions error, confirm your IAM user or role has s3:PutObject permission on this bucket.
Need help? Why is my S3 upload failing?
Confirm your bucket is private
The whole point of this architecture is that nobody can access your files directly through S3. You need to prove this before connecting CloudFront.
- In your bucket, navigate back to the root by clicking your bucket name in the breadcrumb.
- Click index.html to open its Object overview page.
- Find the Object URL field in the Object overview. Copy this URL.
Before you open this URL, take a moment to predict what you will see. Do you think the page will load normally, or will you get an error?
- Paste the Object URL into a new browser tab and press Enter.
You should see an XML error message containing AccessDenied. This confirms your bucket is private.
What is this error telling me?
The AccessDenied response means S3 rejected the request because no valid credentials or bucket policy allows public read access. This is exactly what Block Public Access enforces.
In the next step, you will create a CloudFront distribution with Origin Access Control (OAC) so that CloudFront (and only CloudFront) can read from this bucket and serve the files to visitors worldwide.
Seeing the HTML page instead of an error?
If your browser loads the page successfully, your bucket's Block Public Access settings may be disabled. Go back to your bucket, click the Permissions tab, and confirm that all four Block Public Access settings are turned on.
Still seeing the page load? Help me lock down my S3 bucket.
Your origin is ready. You have a private S3 bucket with a website that nobody can access directly. Next up, you will create a CloudFront distribution that unlocks this content and delivers it to the world from 750+ edge locations.
Create a CloudFront Distribution with OAC
Your S3 bucket is loaded with website files and completely locked down. Anyone trying to reach your content right now would need to hit that single-region bucket directly, which is both slow for distant users and blocked by your security settings.
CloudFront solves this by caching your content at 750+ edge locations around the world. In this step, you'll create a CloudFront distribution and configure Origin Access Control (OAC) so CloudFront can securely read from your private bucket without opening it to the public.
In this step, get ready to:
- Create a CloudFront distribution with your S3 bucket as the origin.
- Configure OAC so only CloudFront can access your private bucket.
- Verify your website loads through the CloudFront domain name.
Start creating a CloudFront distribution
A CDN distribution is the resource that tells CloudFront where to fetch your content and how to deliver it. You'll use the CloudFront console wizard to create one now.
- Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
- Choose Create distribution.
- Enter a Distribution name for your distribution (e.g. My Static Website). This name appears as a tag to help you identify the distribution later.
- Select Single website or app.
- Choose Next.
What is a CloudFront distribution?
A distribution is the main resource in CloudFront. It defines which origin (your S3 bucket) to pull content from, how long to cache that content, and which protocols to allow.
Once created, CloudFront assigns your distribution a unique domain name (like d111111abcdef8.cloudfront.net) that routes visitors to the nearest edge location automatically.
Select your S3 bucket as the origin with OAC
The origin is where CloudFront fetches the original files from. You'll point it at your S3 bucket and let the wizard auto-configure OAC to keep the bucket private.
- On the Origin type page, select Amazon S3.
- For S3 origin, choose Browse S3 and select the bucket you created in Step 1.
- Under Settings, choose Use recommended origin settings.
- Choose Next.
What does Use recommended origin settings do?
This single choice auto-configures several things for you. It sets up Origin Access Control (OAC), enables GZIP compression, applies the CachingOptimized cache policy, redirects HTTP to HTTPS, and restricts allowed methods to GET and HEAD.
Most importantly, CloudFront automatically updates your S3 bucket policy to allow only this distribution to read objects. Your bucket stays private to everyone else.
Why OAC instead of OAI?
You might see references to Origin Access Identity (OAI) in older tutorials. OAC is the newer, recommended approach. It supports all S3 buckets in all AWS Regions (including opt-in Regions), SSE-KMS encryption, and dynamic PUT/DELETE requests that OAI cannot handle.
Disable WAF, create the distribution, and verify deployment
- On the Enable security protections page, choose to disable WAF security protections.
- Choose Next.
- Choose Create distribution.
Why skip WAF?
AWS WAF adds a web application firewall in front of your distribution. It's useful for production, but it costs money and is outside the scope of this project. Disabling it keeps your project within the CloudFront Always Free Tier.
CloudFront now deploys your distribution across its global edge network. This takes 3-5 minutes.
- In the Details section of your new distribution, find the Distribution domain name field. Record this value (it looks like d111111abcdef8.cloudfront.net).
- Watch the Last modified field. Wait until it changes from Deploying to a date and time. This confirms your distribution is live.
Before you visit the URL, what do you expect to see? Remember that OAC was configured automatically and your bucket policy was updated.
- Open a new browser tab and visit your distribution URL, combining your domain name with the path to your homepage:
https://[[DISTRIBUTION_DOMAIN="d111111abcdef8.cloudfront.net"]]/index.html
You should see your website loading through CloudFront. This is the same index.html that returned Access Denied when you tried the direct S3 URL in the previous step. Now it loads perfectly because CloudFront's OAC authenticates the request on your behalf.
Seeing Access Denied or a blank page?
- Make sure the Last modified field shows a date, not Deploying. The distribution needs to finish deploying before it can serve content.
- Double-check that the URL ends with /index.html. CloudFront does not automatically redirect to a default page unless configured separately.
- Confirm you selected Use recommended origin settings during creation. If you chose Customize origin settings instead, OAC may not have been configured automatically.
Need more help? Why is my CloudFront distribution returning Access Denied for my S3 origin?
Your website is now delivered globally through CloudFront's edge network. Next up, you'll see caching in action and discover what happens when you update a file on S3 but the edge still serves the old version.
Observe Caching and the Stale Content Problem
Your CloudFront distribution is live and serving your website from edge locations around the world.
That speed comes from caching. CloudFront stores a copy of your files at edge locations so it can serve them without reaching back to S3 every time. But what happens when you update a file on S3? The edge cache does not know about the change. The SAA-C03 exam tests this caching behavior heavily, so in this step you will experience the stale content problem firsthand.
In this step, get ready to:
- Confirm CloudFront is serving cached content by inspecting the X-Cache response header in your browser.
- Update your homepage in S3 with a visible change.
- Reload the CloudFront URL and observe that the old content still appears.
Inspect the X-Cache response header
CloudFront adds an X-Cache header to every response it serves. This header tells you whether the content came from the edge cache or had to be fetched fresh from the origin. You will use your browser's developer tools to find this header.
- Open your CloudFront URL in your browser (the https://your-distribution-domain.cloudfront.net/index.html URL from the previous step).
- Open developer tools by pressing F12 (works on both macOS and Windows in Chrome), or press Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the Network tab at the top of the developer tools panel.
- Reload the page by pressing Cmd+R (macOS) or Ctrl+R (Windows).
- In the list of requests, click on index.html (the first item in the list).
- Click the Headers tab in the panel that opens.
- Scroll down to the Response Headers section and look for the X-Cache header.
You should see Hit from cloudfront as the value. This confirms CloudFront served the page from its edge cache without contacting S3.
Reload twice if you see a Miss
The very first request after a deployment is always a cache miss because no edge location has a copy yet. If you see Miss from cloudfront on your first reload, simply reload the page one more time. The second request should show Hit from cloudfront.
What does the X-Cache header tell you?
CloudFront includes this header in every response so you can see the caching status at a glance.
- Hit from cloudfront means the content was already cached at the edge location nearest to you. CloudFront served it directly without reaching back to S3.
- Miss from cloudfront means the edge location did not have a cached copy. CloudFront fetched the content from S3, served it to you, and cached it for next time.
Not seeing the X-Cache header?
Make sure you are visiting the CloudFront URL (ending in .cloudfront.net), not the S3 object URL. Only CloudFront adds this header.
If the Network tab is empty, make sure you opened developer tools before reloading the page. DevTools only captures requests that happen while it is open.
Help me find the X-Cache header
Update your homepage in S3
Now that you have confirmed caching is active, you will change the origin content on S3. This sets up the stale content scenario: S3 will have the new version, but CloudFront's edge cache will still hold the old one.
- Open the index.html file on your computer (the same file you uploaded in Step 1).
- Change the main heading text to something visibly different. For example, change it from your original heading to Welcome to CloudFront or any other text you will recognize immediately.
- Save the file on your computer.
- Return to the S3 console in your browser and navigate to your bucket.
- Click Upload.
- Click Add files and select your updated index.html file.
- Click Upload to overwrite the original file.
You should see a green success banner confirming the upload completed. Your S3 bucket now holds the updated version of index.html.
Upload failed or file not appearing?
Make sure you are uploading to the root of your bucket, not inside the images/ folder. The file name must be exactly index.html to overwrite the original.
Help me troubleshoot this upload
Reload CloudFront and hit the stale content problem
Your S3 bucket now has the new heading text. Before you reload your CloudFront URL, take a moment to predict what you expect to see. Do you think the updated heading will appear, or will you still see the old version?
- Switch back to the browser tab with your CloudFront URL.
- Make sure the Network tab in developer tools is still open.
- Reload the page.
- Look at your website content and check the X-Cache header for index.html in the Response Headers section.
The old heading text still appears. The X-Cache header shows Hit from cloudfront. CloudFront served the cached copy from the edge and never checked S3 for updates. You have hit the stale content problem.
Why does CloudFront ignore your S3 update?
CloudFront caches objects at edge locations based on TTL (Time to Live). The default TTL for an S3 origin without Cache-Control headers is 24 hours (86400 seconds). Until that time expires, CloudFront assumes its cached copy is still valid and never contacts S3.
This is the fundamental trade-off of a CDN. Caching makes content load instantly from 750+ edge locations worldwide. But it also means origin updates are invisible until the cache expires or you manually force a refresh.
Why does this matter for the SAA-C03?
The exam tests whether you understand that updating an S3 object does not immediately update what CloudFront serves. You need to know two solutions: cache invalidation (forces immediate refresh) and versioned file names (e.g. style-v2.css) which sidestep caching entirely. You will perform a cache invalidation in the next step.
You have now witnessed the stale content problem firsthand. Your S3 bucket has the new file, but CloudFront's edge cache refuses to let go of the old version. In the next step, you will force CloudFront to discard its cached copy using cache invalidation.
Invalidate the Cache
You proved in the last step that CloudFront's edge cache keeps serving stale content even after you update the file in S3. That gap between "uploaded" and "visible" is exactly what cache invalidation closes.
Cache invalidation tells CloudFront to discard its cached copy of a file so the next viewer request fetches the latest version from your origin. This is one of the most heavily tested concepts on the SAA-C03 exam.
In this step, get ready to:
- Create a cache invalidation for your updated page.
- Verify CloudFront serves the fresh content.
- Observe the X-Cache header cycle from Miss back to Hit.
Create a cache invalidation
A cache invalidation removes the stale copy from every edge location that cached it. On the next request, CloudFront goes back to S3 for the fresh version.
- Return to the CloudFront console and choose your distribution from the list.
- Choose the Invalidations tab.
- Choose Create invalidation.
- In the invalidation path field, enter /index.html (one path per line).
- Choose Create invalidation.
Watch the Status column. It starts as InProgress and changes to Completed within a minute or two.
What is cache invalidation?
When you invalidate a path, CloudFront marks the cached object as expired at every edge location worldwide. The next viewer request triggers a fresh fetch from your S3 origin.
The first 1,000 invalidation paths you submit per month are free. After that, each additional path costs $0.005. A wildcard path like /* counts as a single path even if it matches thousands of files.
Why invalidation instead of versioned file names?
AWS recommends versioned file names (e.g. style-v2.css) for frequent updates because they avoid invalidation costs and work even when viewers have local browser caches. Invalidation is best for urgent, one-off fixes where you cannot change the file name.
The SAA-C03 tests this distinction. If a question mentions frequent content updates, versioning is the answer. If it mentions an emergency content removal, invalidation is the answer.
Verify the fresh content loads
Before you reload, think about what you expect to see. Will the CloudFront URL show the old heading or the new one? What should the X-Cache header say on the very first request after invalidation?
- Switch to the browser tab where your CloudFront URL is open.
- Open developer tools by pressing F12 (Windows) or Cmd+Option+I (macOS).
- Go to the Network tab.
- Reload the page.
- Click on the index.html request and look at the response headers.
You should now see your updated heading text on the page. The X-Cache header shows Miss from cloudfront. This confirms the edge location discarded its old copy and fetched the fresh version from S3.
Still seeing the old content after invalidation?
- Make sure the invalidation status shows Completed in the CloudFront console. If it still says InProgress, wait another minute and reload.
- Try a hard refresh (Ctrl+Shift+R on Windows or Cmd+Shift+R on macOS) to bypass your browser's local cache.
- Confirm the invalidation path matches your file exactly. The path /index.html is case-sensitive and must include the leading slash.
Why is my invalidation not working?
- Reload the page one more time.
- Check the X-Cache header again. It now shows Hit from cloudfront. The new version is cached at the edge.
What does the Miss-then-Hit cycle prove?
The first reload after invalidation is always a Miss because the edge cache is empty for that path. CloudFront fetches the object from S3, serves it to you, and caches it. Every subsequent request is a Hit until the TTL expires or you invalidate again.
This lifecycle (Miss on first request, Hit on repeats, stale after TTL, Miss again after invalidation) is the caching mental model the SAA-C03 expects you to have.
You have now completed the full caching lifecycle: cache hit, stale content, invalidation, fresh fetch, and re-cache. In the Secret Mission, you will create a custom cache behavior with a different path pattern and TTL settings to control how aggressively images are cached.
Secret mission
Add a Custom Cache Behavior for Images
Real-world distributions use multiple cache behaviors to apply different caching rules for different content types. Images rarely change and should cache longer. HTML changes frequently and should cache shorter. Can you create a second cache behavior with a custom path pattern and aggressive TTL settings?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses CloudFront's always-free tier (1 TB data transfer out and 10 million requests per month) and S3 storage costs are negligible for a few files, so there are no meaningful ongoing costs.
Resources you used:
- CloudFront distribution (with OAC, default behavior, and /images/* behavior).
- CloudFront custom cache policy (AggressiveImageCache).
- S3 bucket (containing index.html, error.html, and images/ folder).
Keep everything running
No action needed. Choose this if you are still actively building or plan to explore CloudFront further.
- Your CloudFront distribution remains active and serves cached content from edge locations.
- Your S3 bucket retains all uploaded files.
- Your custom cache policy stays available for future distributions.
Pause - I'll come back to this later
Disable your distribution to stop serving content. Your configuration is preserved so you can re-enable it later.
- Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
- Select the checkbox next to your distribution.
- Choose Disable and confirm when prompted.
- Wait until the Last modified column shows a new timestamp. This confirms the distribution is fully disabled.
- Your S3 bucket and cache policy remain intact. When you are ready to resume, select the distribution and choose Enable.
Delete - I don't want to use this again
Remove all project resources permanently. This cannot be undone.
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 and confirm when prompted.
- Wait until the Last modified column shows a new timestamp. This may take a few minutes.
- Select the checkbox for the disabled distribution again.
- Choose Delete and confirm the deletion when prompted.
Delete the custom cache policy:
- In the CloudFront console, open the Policies page from the left navigation pane.
- Find your AggressiveImageCache policy in the list of custom cache policies.
- Select the radio button next to the policy and choose Delete. Confirm the deletion when prompted.
Empty and delete the S3 bucket:
- Open the Amazon S3 console at https://console.aws.amazon.com/s3/.
- In the left navigation pane, choose General purpose buckets.
- Select your bucket and choose Empty.
- Type the confirmation text shown on the page into the field, then choose Empty.
- Return to the buckets list. Select your (now empty) bucket and choose Delete.
- Type your bucket name in the confirmation field, then choose Delete bucket.
Nice Work!
Nice Work!
You just built a production-style CDN architecture that delivers a private S3 website through CloudFront's global edge network. Your site is now secured with Origin Access Control and served from 750+ edge locations worldwide.
You've learned how to:
- Deploy a CloudFront distribution with Origin Access Control (OAC) to securely serve content from a completely private S3 bucket, proving that direct S3 URLs return Access Denied while the same content loads through CloudFront.
- Observe edge caching in action using the X-Cache response header, and experience the stale content problem firsthand when S3 updates do not appear immediately through CloudFront.
- Perform a cache invalidation to force CloudFront to fetch fresh content from the origin, and understand when to invalidate vs. when to use versioned file names.
- Secret Mission: Create a custom cache behavior with the /images/* path pattern and a custom TTL policy to demonstrate per-path caching strategies used in real-world distributions.
Ready to quiz yourself?