Deliver a Static Website with CloudFront
Build a local S3 site delivered through a LocalStack CloudFront distribution.
Introduction
30 Second Summary
A website can look perfect on your Mac but lose its design when one referenced file never reaches its destination. That missing piece reveals how browsers retrieve every page asset separately.
In this project, you will build a status-card website that moves from local files into S3. You will deliver it through a CloudFront distribution running in LocalStack for AWS on your Mac.
What You'll Build
Picture opening a CloudFront URL in Safari to see your styled status card arrive through a delivery pipeline running on your Mac.
By the end of this project, you'll have:
- A styled status-card website that displays two green delivery badges and a visible release number.
- A working S3-to-CloudFront delivery path that serves the website through a viewer URL.
- A repeatable publishing workflow that updates the site to Release: v2 and submits a CloudFront invalidation request.
- Secret Mission: Enable S3 Versioning. Then recover the website after an accidental overwrite by restoring an earlier object version.
Are there any prerequisites?
You'll need a Mac with internet access plus Docker Desktop, Homebrew, Visual Studio Code, and an existing LocalStack Base-or-higher workspace. The project uses your existing LocalStack subscription and creates no AWS usage charges.
Before We Start
This quick checkpoint connects S3 storage with CloudFront delivery before the hands-on work begins. It gives each later action a clear purpose.
Start Your LocalStack Environment
Your website will depend on a local request path through S3 and CloudFront. That path needs one predictable LocalStack runtime before you create any resources.
A project-local configuration pins the AWS emulator to 2026.10. Homebrew installs lstk so Docker Desktop can run that version with persistence enabled.
In this step, get ready to:
- Create the project folders with a pinned emulator configuration.
- Prepare lstk 1.4.2 by passing its pre-flight checks.
- Start the authenticated emulator with persistent state.
Create the pinned project structure
The .lstk/config.toml file is a project-specific TOML configuration. It keeps the emulator version consistent whenever you start this project.
- Press Command-Space bar to open Spotlight.
- Type Visual Studio Code into the search field.
- Press Return to open Visual Studio Code.
You will see the Visual Studio Code window ready for a project folder.
- Click File in the top menu.
- Select Open Folder....
- Select your Desktop location.
- Select New Folder.
- Enter s3-cloudfront-local as the folder name.
- Press Return to create the folder.
- Select the new s3-cloudfront-local folder.
- Select Open.
You will see S3-CLOUDFRONT-LOCAL at the top of the Explorer sidebar.
- Select the S3-CLOUDFRONT-LOCAL heading in the Explorer sidebar.
- Select the New Folder icon in the Explorer toolbar.
- Enter .lstk as the folder name.
- Press Return to create the folder.
You will see the .lstk folder under the project heading.
- Select the S3-CLOUDFRONT-LOCAL heading again.
- Select the New Folder icon.
- Enter www as the folder name.
- Press Return to create the folder.
The Explorer now shows separate .lstk and www folders.
- Select the .lstk folder in the Explorer sidebar.
- Select the New File... icon.
- Enter config.toml as the file name.
- Press Return to create the file.
- Add the pinned AWS emulator configuration by pasting this code:
[[containers]]
type = "aws"
tag = "2026.10"
port = "4566"
What does this configuration do?
- The [[containers]] line begins the emulator definition for this project.
- The type = "aws" setting selects the AWS emulator.
- The tag = "2026.10" setting locks startup to the project version.
- The port = "4566" setting exposes the emulator on its configured local port.
- Save .lstk/config.toml by pressing Command-S.
- Select the .lstk folder in the Explorer sidebar.
You will see config.toml inside .lstk.
✔️ Awesome, I've got everything!
Your project structure is saved with the pinned emulator configuration.
ⓧ I'd like to double check the full code
The complete .lstk/config.toml file should match this reference.
[[containers]]
type = "aws"
tag = "2026.10"
port = "4566"
What should match?
Compare the container type with the version tag. Check the port value before continuing.
Cannot see the configuration file?
- Expand the .lstk folder in the Explorer sidebar.
- Check that the file name is exactly config.toml.
- Save the file if its editor tab shows an unsaved-change dot.
Help me check my LocalStack project structure.
Install and diagnose lstk
The lstk CLI manages authentication plus the emulator lifecycle. Its diagnostic check catches local runtime problems before startup.
- Click View in the Visual Studio Code menu.
- Select Terminal.
You will see an integrated terminal at the bottom of Visual Studio Code. It starts inside the s3-cloudfront-local folder.
- Check whether lstk is already available by running this command:
lstk --version
What does this command do?
This asks the installed CLI to print its version. The result determines which setup path you need.
Choose the tab that matches the result in your terminal.
✔️ I see version 1.4.2
Your CLI already matches the project version. You can continue to the shared verification below.
ⓧ I see an older version
Homebrew can update an outdated formula through the same installation path.
- Install the current LocalStack tap formula by running:
brew install localstack/tap/lstk
What does this command do?
Homebrew installs the current lstk formula from the LocalStack tap. An outdated installation is upgraded to the available release.
ⓧ Command not found
The missing command means the CLI still needs to be installed.
- Install lstk from the LocalStack tap by running:
brew install localstack/tap/lstk
What does this command do?
Homebrew downloads the lstk formula from the LocalStack tap. It then makes the CLI available in your terminal.
- Verify the selected setup path by running the version check again:
lstk --version
Why check again?
The version check confirms that your terminal can find the installed CLI. It also confirms that the installation produced the required release.
You should see version 1.4.2 in the output.
Homebrew installation failing?
- Confirm that your Mac has an active internet connection.
- Read the final Homebrew output for the first failed installation step.
- Repeat the installation after resolving the reported issue.
Help me diagnose my lstk installation.
A successful installation can still meet a blocked network or an unavailable container engine. The doctor check tests those startup dependencies before they interrupt the emulator.
- Run the LocalStack pre-flight checks with this command:
lstk doctor
What does the doctor check?
- It checks DNS plus HTTPS connectivity.
- It checks proxy plus TLS interception settings.
- It checks the container engine.
- It checks available memory plus disk space.
Each pre-flight check should pass. A failed check includes a finding that points to the affected part of your environment.
Seeing a failed pre-flight check?
- Use Spotlight to open Docker Desktop if the container engine check fails.
- Follow the displayed network fix if the DNS or HTTPS check fails.
- Repeat the diagnostic check after applying the displayed fix.
Help me resolve my lstk doctor result.
Once every check passes, your Mac is ready to run the pinned emulator.
Start and verify the emulator
A persistent state keeps your emulated resources available across later stops and starts. This protects the S3 bucket plus CloudFront distribution you create in the next steps.
The first start opens a browser-based authentication flow for your existing LocalStack workspace. This connects the CLI to your licensed account while the emulator remains on your Mac.
The initial image pull can take a few minutes after login. A quiet wait during that download is expected.
- Keep your existing LocalStack Base-or-higher account ready for the browser flow.
- Start the pinned emulator with persistent state by running:
lstk start --persist
What happens during startup?
- The CLI reads .lstk/config.toml from the current project.
- The configuration selects the AWS emulator tagged 2026.10 on port 4566.
- The --persist option keeps emulator state across restarts.
- Complete the browser sign-in with your existing LocalStack account when prompted.
- Return to the Visual Studio Code terminal after the browser confirms authentication.
The startup command returns once the emulator is ready to serve requests.
Before you check the final status, which emulator version and persistence state do you expect to see?
- Check the running emulator by running this command:
lstk status
What does the status show?
The status command reports the running endpoint plus emulator version. It also reports whether persistent state is enabled.
You should see that the LocalStack AWS Emulator is running. Its version should be 2026.10.
The output should include Persistence: Enabled.
That is the setup locked down: your authenticated emulator is running with the exact project version.
Status does not show the running emulator?
- Confirm that Docker Desktop is running on your Mac.
- Check that your terminal still belongs to the s3-cloudfront-local workspace.
- Review the startup output for an authentication or image-download finding.
Help me fix my LocalStack startup.
Your local AWS environment is pinned and ready for resources. Next up, you will build the website that S3 stores and CloudFront delivers.
Build the Website Locally
Your LocalStack emulator is running with persistent state. Your project now has a stable local environment for the website that it will deliver.
The complete local site becomes your visual baseline. Later, that baseline helps you spot exactly what changes when a referenced asset is missing from storage.
In this step, get ready to:
- Build the website structure inside the existing project.
- Style the status card with a gradient background and responsive layout.
- Open the finished website locally to establish a visual reference.
Open the project and build the page
An HTML document defines the content that Safari displays. This page also links to a separate stylesheet so each browser request has a clear file behind it.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter.
- Click File in the top menu bar.
- Click Open Folder....
- Select the existing s3-cloudfront-local project directory from the location you used in Step 1.
- Click Open.
Seeing a Workspace Trust dialog?
This dialog appears when Visual Studio Code opens a project directory for the first time. Select Yes, I trust the authors because you created these project files yourself.
- Expand the www directory in the Explorer view.
- Select New File... from the Explorer toolbar.
- Type index.html and press Enter.
- Create the page shell by pasting this code into www/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>S3 and CloudFront Lab</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
</html>
What does this page shell do?
- The document metadata gives the browser the page title and its mobile viewport settings.
- The stylesheet link tells the browser to request style.css from the same directory.
- The empty <body> provides the location for the visible status card.
- Save www/index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Check the expanded www directory in the Explorer view.
You should see index.html listed beneath the www directory.
Don't see index.html?
- Expand the www directory by selecting the arrow beside its name.
- Check that index.html appears inside www instead of beside it.
Ask for help if the file is in the wrong location: Help me move index.html into the www directory in Visual Studio Code.
- Select the empty <body> element in www/index.html.
- Replace that element with the complete status-card body below:
<body>
<main class="status-card">
<p class="eyebrow">LOCAL AWS DELIVERY PIPELINE</p>
<h1>S3 + CloudFront Lab</h1>
<p class="summary">
This page is stored as S3 objects and delivered through a LocalStack CloudFront distribution.
</p>
<section class="status-grid" aria-label="Delivery status">
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>S3 origin</h2>
<p>index.html and style.css are available as separate object keys.</p>
</article>
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>CloudFront viewer</h2>
<p>The distribution retrieves each requested object from the origin.</p>
</article>
</section>
<p id="release" class="release">Release: v1</p>
</main>
</body>
How is the card structured?
- The status-card element groups the page into one central card.
- The status-grid element holds separate panels for the origin and viewer.
- Each status-dot element becomes a green status badge once the stylesheet is applied.
- The release element identifies this first local build as Release: v1.
- Save www/index.html.
- Press Cmd+F (macOS) or Ctrl+F (Windows).
- Search for Release: v1.
Visual Studio Code should highlight the release line near the bottom of www/index.html.
Can't find the release line?
- Confirm that you replaced the complete empty <body> element.
- Check that the pasted section ends with the closing </body> tag.
Ask for help comparing the structure: Help me find the missing status-card HTML in my index.html file.
✔️ Awesome, I've got everything!
Your page structure is saved inside www/index.html.
ⓧ I'd like to double check the full code
Compare your www/index.html file with this complete version.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>S3 and CloudFront Lab</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="status-card">
<p class="eyebrow">LOCAL AWS DELIVERY PIPELINE</p>
<h1>S3 + CloudFront Lab</h1>
<p class="summary">
This page is stored as S3 objects and delivered through a LocalStack CloudFront distribution.
</p>
<section class="status-grid" aria-label="Delivery status">
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>S3 origin</h2>
<p>index.html and style.css are available as separate object keys.</p>
</article>
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>CloudFront viewer</h2>
<p>The distribution retrieves each requested object from the origin.</p>
</article>
</section>
<p id="release" class="release">Release: v1</p>
</main>
</body>
</html>
Style the website in layers
A CSS stylesheet controls the page's layout and appearance. Building it in layers gives you a quick visual check after each group of styles.
- Select the www directory in the Explorer view.
- Select New File... from the Explorer toolbar.
- Type style.css and press Enter.
- Set the page foundation by pasting this code into www/style.css:
:root {
color-scheme: light;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f172a;
color: #0f172a;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
background: linear-gradient(135deg, #0f172a 0%, #1d4ed8 55%, #38bdf8 100%);
}
What do these foundation styles do?
- The root styles establish the page font and default colours.
- The universal selector keeps padding inside each element's declared size.
- The body styles fill the browser window with a blue gradient.
- The grid layout places the website content in the centre of the page.
- Save www/style.css.
- Control-click www/index.html in the Explorer view.
- Select Reveal in Finder.
- Control-click index.html in Finder.
- Select Open With.
- Select Safari.
Safari should show the website content centred against a blue gradient background.
Still seeing a plain background?
- Check that the stylesheet is named exactly style.css.
- Confirm that style.css sits beside index.html inside the www directory.
- Refresh Safari after saving the stylesheet.
Ask for help with the file link: Help me work out why index.html is not loading style.css.
- Switch back to www/style.css in Visual Studio Code.
- Add the card and heading styles below the existing body block:
.status-card {
width: min(760px, 100%);
padding: 40px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
}
.eyebrow {
margin: 0 0 10px;
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
}
h1 {
margin: 0;
font-size: clamp(2.2rem, 8vw, 4.4rem);
line-height: 1;
}
How does the card take shape?
- The status-card styles create a white panel with rounded corners and a shadow.
- The eyebrow styles turn the pipeline label into a compact blue heading.
- The heading size adapts to the browser width while keeping a readable upper limit.
- Save www/style.css.
- Refresh the page in Safari.
You should see the website content inside a white rounded card. The main heading should also appear larger.
Don't see the white card?
- Check that the selector starts with .status-card.
- Confirm that www/index.html contains class="status-card".
Ask for help matching the selector: Help me fix the status-card styles that are not appearing in Safari.
- Add the summary and responsive grid styles at the end of www/style.css:
.summary {
max-width: 60ch;
margin: 20px 0 30px;
color: #475569;
font-size: 1.05rem;
line-height: 1.7;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 16px;
}
.status-grid article {
padding: 20px;
border: 1px solid #dbeafe;
border-radius: 16px;
background: #eff6ff;
}
How does the grid respond?
- The summary styles limit each text line to a readable width.
- The grid automatically fits as many panels as the available width allows.
- Each article becomes a pale blue panel with its own border and spacing.
- Save www/style.css.
- Refresh the page in Safari.
You should see two pale blue status panels inside the white card. They sit side by side when the browser is wide enough.
Are the panels still bare?
- Confirm that both panels are inside the status-grid section in www/index.html.
- Check that the article selector reads .status-grid article.
Ask for help with the grid: Help me troubleshoot why my status-grid article styles are not showing.
Complete the local visual reference
The final style group sharpens the panel text and adds the green status indicators. It also separates the release label from the grid.
- Add the final panel and release styles at the end of www/style.css:
.status-grid h2 {
margin: 12px 0 8px;
font-size: 1.05rem;
}
.status-grid p {
margin: 0;
color: #475569;
line-height: 1.5;
}
.status-dot {
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.16);
}
.release {
margin: 26px 0 0;
color: #64748b;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9rem;
}
What finishes the design?
- The panel text styles create consistent spacing and readable contrast.
- The status-dot styles turn each empty span into a green circular badge.
- The release styles give the release label its own spacing and monospace font.
- Save www/style.css.
Before you refresh, what visual details do you expect the finished page to show?
- Refresh the local page in Safari.
You should see a blue gradient behind a white card. The heading should read S3 + CloudFront Lab.
Two green status badges should appear inside the card. The release label should read Release: v1.
You have a complete local reference for the deployment checks ahead. Every missing visual detail now has something concrete to compare against.
Does the final page look different?
- Confirm that both files are saved inside the www directory.
- Check that www/index.html links to style.css.
- Compare each CSS selector with the class names in www/index.html.
Ask for help comparing the final files: Help me diagnose why my local status-card website looks different from the expected design.
✔️ Awesome, I've got everything!
Great. Save both files so this styled page remains your local reference.
ⓧ I'd like to double check the full code
Compare your www/style.css file with this complete version.
:root {
color-scheme: light;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f172a;
color: #0f172a;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
background: linear-gradient(135deg, #0f172a 0%, #1d4ed8 55%, #38bdf8 100%);
}
.status-card {
width: min(760px, 100%);
padding: 40px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
}
.eyebrow {
margin: 0 0 10px;
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
}
h1 {
margin: 0;
font-size: clamp(2.2rem, 8vw, 4.4rem);
line-height: 1;
}
.summary {
max-width: 60ch;
margin: 20px 0 30px;
color: #475569;
font-size: 1.05rem;
line-height: 1.7;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 16px;
}
.status-grid article {
padding: 20px;
border: 1px solid #dbeafe;
border-radius: 16px;
background: #eff6ff;
}
.status-grid h2 {
margin: 12px 0 8px;
font-size: 1.05rem;
}
.status-grid p {
margin: 0;
color: #475569;
line-height: 1.5;
}
.status-dot {
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.16);
}
.release {
margin: 26px 0 0;
color: #64748b;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9rem;
}
Your local website is ready. Next, you will place only index.html in S3 so CloudFront can expose what happens when the stylesheet is missing.
Deploy an Incomplete Site
Your styled website now gives you a visual reference for the deployment. The next test moves its HTML into a LocalStack delivery path.
A CloudFront distribution retrieves requested files from an S3 bucket. Each viewer request must match a separate object key in that bucket. You will test this relationship with an incomplete deployment.
In this step, get ready to:
- Create an S3 bucket with a public-read bucket policy.
- Upload only the HTML object to the bucket.
- Create a CloudFront distribution that exposes the incomplete deployment.
Create the S3 bucket policy
A bucket policy controls which requests can retrieve objects from a bucket. This local policy allows public read requests for objects stored under testwebsite.
This public access exists only inside your local emulator. It does not change an AWS account.
- In the file sidebar of Visual Studio Code, create bucket_policy.json inside the s3-cloudfront-local folder.
You should see bucket_policy.json beside the existing project files.
- Paste the public-read policy into bucket_policy.json using the code below:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::testwebsite/*"
}
]
}
What does this policy do?
- The Statement defines one permission rule for the bucket.
- The s3:GetObject action permits read requests for stored objects.
- The resource value limits the permission to object keys under testwebsite.
- The Principal value makes those local objects publicly readable.
- Save bucket_policy.json.
The saved file now contains the permission that your local distribution needs to retrieve website objects.
✔️ Awesome, I've got everything!
Great. Your saved policy is ready to attach to the bucket.
ⓧ I'd like to double check the full code
Compare your complete bucket_policy.json file with this reference.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::testwebsite/*"
}
]
}
- Return to the terminal from earlier.
- Create the testwebsite bucket by running this command:
lstk aws s3api create-bucket --bucket testwebsite
What does this command do?
This command asks the running LocalStack emulator to create an S3 bucket named testwebsite.
- Attach the saved bucket policy by running this command:
lstk aws s3api put-bucket-policy --bucket testwebsite --policy file://bucket_policy.json
How is the policy attached?
The command reads bucket_policy.json from your project folder. LocalStack stores that policy against the testwebsite bucket.
That is the access layer ready. Your local bucket can now provide public objects to the distribution you create later in this step.
Bucket Or Policy Command Failing?
- Confirm the LocalStack AWS Emulator is still running.
- Confirm bucket_policy.json appears inside the s3-cloudfront-local folder.
- Confirm the filename uses an underscore between bucket and policy.
Help me diagnose my local S3 bucket or bucket policy setup.
Upload only the HTML object
Uploading a file to S3 stores it as an object. The destination path determines the object key that CloudFront can request.
- Upload the local www/index.html file by running this command:
lstk aws s3 cp www/index.html s3://testwebsite/
What does this upload do?
The command copies the local HTML file into the testwebsite bucket. Its destination object key is index.html.
- Inspect the bucket's exact object inventory by running this command:
lstk aws s3 ls s3://testwebsite/
What does this listing prove?
The listing shows every object key currently available from this origin bucket. A browser request can only retrieve a key that appears here.
You should see index.html as the only object name. The local style.css file does not appear in the bucket listing.
Good. You now have a precise snapshot of what the origin can serve.
Object Listing Look Wrong?
- Confirm the upload source is www/index.html.
- Confirm the destination bucket name is testwebsite.
- Confirm the object listing contains no style.css entry.
Help me inspect why my LocalStack S3 object listing differs from this step.
Create the CloudFront distribution
An origin is the location where a distribution retrieves content. Your origin domain points CloudFront at the local testwebsite bucket.
- Create the distribution by running this command:
lstk aws cloudfront create-distribution --origin-domain-name testwebsite.s3.amazonaws.com
What does this command create?
- The origin domain identifies the S3 bucket that supplies requested objects.
- The returned distribution ID identifies this CloudFront configuration.
- The returned distribution domain provides the viewer address for browser requests.
The response contains Distribution.Id plus Distribution.DomainName.
- Record the returned Distribution.Id here: your distribution ID.
- Record the returned Distribution.DomainName here: your distribution domain.
Distribution Creation Failing?
- Confirm the LocalStack AWS Emulator is still running.
- Confirm your existing LocalStack workspace includes CloudFront access.
- Confirm the origin domain contains the authored bucket name testwebsite.
Help me troubleshoot my local CloudFront distribution creation.
The generated viewer domain can take a short time to resolve on macOS. A first loading failure can mean that local DNS is still catching up.
Before you load the viewer URL, do you expect the polished local styling to survive this incomplete deployment?
- Load the viewer URL in Safari by entering:
https://[[DISTRIBUTION_DOMAIN="your distribution domain"]]/index.html
How does this viewer URL work?
- The distribution domain directs the request to your local CloudFront distribution.
- The /index.html path asks the origin for the index.html object key.
You should see the S3 + CloudFront Lab content through the distribution. The page is visibly unstyled.
This shortfall is intentional. The HTML arrived through CloudFront because index.html exists in S3.
Why is the page unstyled?
- The first viewer request asks for /index.html.
- The returned HTML references style.css.
- The browser makes a second viewer request for that stylesheet.
- The origin has no matching style.css object key.
Viewer Domain Not Loading?
- Wait briefly if the generated distribution domain has not resolved.
- Reload the same viewer URL.
- Confirm the recorded domain matches Distribution.DomainName without extra spaces.
Help me troubleshoot why my LocalStack CloudFront viewer domain is not loading.
You have proved that one web page can trigger multiple origin lookups. Next up, you will add the missing object key and watch the full design return.
Fix the Missing S3 Object
Your LocalStack CloudFront distribution now serves index.html from the testwebsite S3 bucket. The unstyled page proved that the HTML object is available.
The browser also makes a separate viewer request for style.css. That request needs a matching object key.
In this step, get ready to:
- Upload www/style.css to the testwebsite bucket.
- Confirm index.html and style.css exist as separate object keys.
- Refresh the CloudFront viewer URL to verify the styled site.
Upload the missing stylesheet
The HTML references style.css. The bucket therefore needs an object with that exact key.
- Switch back to the terminal panel from earlier.
- Upload the missing stylesheet by running this command:
lstk aws s3 cp www/style.css s3://testwebsite/
What does this upload do?
- The s3 cp operation copies the local stylesheet into the existing bucket.
- The trailing slash keeps style.css as the object's key.
The terminal confirms that the local stylesheet was copied into the bucket.
Stylesheet upload failing?
- Confirm that the terminal is still inside the s3-cloudfront-local folder.
- Check that www/style.css appears in the Explorer sidebar in Visual Studio Code.
Get help with this stylesheet upload. Share the command output in the NextWork community if the issue continues.
Confirm the object key and metadata
The upload command reports a transfer. The bucket listing proves what the origin can retrieve.
- List the bucket's object keys by running this command:
lstk aws s3 ls s3://testwebsite/
What does this listing prove?
- The s3 ls operation reads the objects currently stored in testwebsite.
- Each listed filename is a key that the CloudFront origin can request.
You have closed the missing-object gap. The bucket now lists index.html and style.css.
- Inspect the stylesheet's object metadata by running this command:
lstk aws s3api head-object --bucket testwebsite --key style.css
What does this metadata check prove?
- The head-object operation requests metadata without downloading the file body.
- A returned response confirms that S3 can find the exact style.css key.
You'll see metadata for the style.css object. This confirms that the stylesheet is ready for origin requests.
Metadata request failing?
- Check that the bucket name is exactly testwebsite.
- Check that the key is exactly style.css with lowercase letters.
- Review the previous object listing to confirm that the upload completed.
Help me diagnose the missing object metadata. Share the listing output in the NextWork community if you need more help.
Refresh the CloudFront site
The HTML already tells the browser to request style.css. The matching key can now complete that second viewer request.
Before you refresh, consider whether the page will stay plain or regain its visual design.
- Return to the Safari tab from earlier.
- Confirm that the address bar shows https://DISTRIBUTION_DOMAIN/index.html with your recorded distribution domain.
- Press Cmd+R (macOS) or Ctrl+R (Windows) to refresh the page.
You'll see a blue gradient background around a white status card. Two green status badges appear inside the card.
The page still shows Release: v1.
Site still looks unstyled?
- Confirm that the page URL uses your recorded Distribution.DomainName value.
- Review the object listing from the previous substep for style.css.
- Refresh the CloudFront page once more.
Help me trace why the stylesheet is still missing. Share a screenshot in the NextWork community if the page stays unstyled.
You fixed the complete delivery path. Both viewer requests now resolve to S3 objects.
How do the two requests travel?
- The first viewer request asks CloudFront for index.html.
- The returned HTML references style.css.
- That reference triggers a second viewer request for style.css.
- CloudFront forwards each requested path to testwebsite.s3.amazonaws.com.
- Each requested path now maps to a key in testwebsite.
Both viewer requests now reach matching S3 objects. Next up, you'll publish a new release before practising invalidation.
Publish an Update and Invalidate
Your styled site already travels through a CloudFront distribution in LocalStack. The next job is to publish a new release without changing its design.
A real CloudFront distribution can retain an older response in its cache after the S3 origin changes. This makes an object upload different from an invalidation.
LocalStack does not implement CloudFront object caching. You will still submit an invalidation so you can practise the production workflow.
In this step, get ready to:
- Publish the updated www/index.html object to S3.
- Confirm LocalStack CloudFront serves the update immediately.
- Create a CloudFront invalidation for every viewer path.
Update the release label
The local www/index.html file in Visual Studio Code is your release source. This update changes only the release label.
- Switch back to the www/index.html tab in Visual Studio Code.
- Find the line containing Release: v1.
- Replace v1 with v2.
- Save www/index.html by pressing Cmd+S.
✔️ My update is saved
Your saved file now matches the release you are about to publish.
ⓧ I'd like to double check the full code
- Compare your complete www/index.html file with this reference:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>S3 and CloudFront Lab</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="status-card">
<p class="eyebrow">LOCAL AWS DELIVERY PIPELINE</p>
<h1>S3 + CloudFront Lab</h1>
<p class="summary">
This page is stored as S3 objects and delivered through a LocalStack CloudFront distribution.
</p>
<section class="status-grid" aria-label="Delivery status">
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>S3 origin</h2>
<p>index.html and style.css are available as separate object keys.</p>
</article>
<article>
<span class="status-dot" aria-hidden="true"></span>
<h2>CloudFront viewer</h2>
<p>The distribution retrieves each requested object from the origin.</p>
</article>
</section>
<p id="release" class="release">Release: v2</p>
</main>
</body>
</html>
What should you compare?
The release paragraph now contains Release: v2. Every other line stays unchanged.
- Return to the terminal from earlier.
- Upload the updated HTML object to S3 by running:
lstk aws s3 cp www/index.html s3://testwebsite/
What does this upload change?
- The command replaces the current index.html object in the testwebsite bucket.
- The local www/index.html file remains your editable source.
- The style.css object remains unchanged.
Your terminal shows transfer output for www/index.html. The S3 origin now holds the updated release.
Upload not completing?
- Confirm your terminal is inside the s3-cloudfront-local folder.
- Confirm LocalStack remains running with persistence enabled.
- Run the upload command above again after correcting the failed check.
Help me troubleshoot why my updated HTML file is not uploading to the LocalStack S3 bucket.
Confirm LocalStack serves the update
The viewer URL still points to the same distribution. Refreshing it reveals whether LocalStack retrieves the updated origin object.
Before you refresh, do you expect the viewer to show the previous release or the newly uploaded release?
- Switch back to the CloudFront viewer page in Safari.
- Refresh the page by pressing Cmd+R.
There it is. You can now see Release: v2 at the bottom of the status card.
Why does the update appear immediately?
LocalStack CloudFront does not cache objects. Its viewer request can retrieve the current S3 object immediately.
Real CloudFront may continue serving an older cached response after an origin upload. An invalidation removes matching cached copies before a later request returns to the origin.
Submit and inspect an invalidation
A CloudFront invalidation is a separate request from uploading an S3 object. The quoted "/*" path targets every viewer path for the recorded distribution.
- Enter the Distribution.Id you recorded earlier here: your recorded Distribution.Id.
Before you submit the invalidation, what do you expect the response to tell you about the request?
- Submit the wildcard invalidation by running:
lstk aws cloudfront create-invalidation --distribution-id [[DISTRIBUTION_ID="your recorded Distribution.Id"]] --paths "/*"
What does this command do?
- The distribution ID directs the request to your existing CloudFront distribution.
- The quoted "/*" path selects every viewer path.
- LocalStack creates an invalidation record for you to inspect.
- Real CloudFront removes matching cached copies before the next origin fetch.
You will receive an invalidation object containing an invalidation ID. The response also includes its status.
- Inspect the returned object for its invalidation ID.
- Inspect the returned object for its status.
Invalidation request failing?
- Confirm you entered the recorded Distribution.Id instead of the Distribution.DomainName.
- Keep the quotation marks around "/*" so your shell passes the wildcard unchanged.
- Confirm the LocalStack AWS Emulator remains running.
Help me troubleshoot why my LocalStack CloudFront invalidation request is failing.
Before the final refresh, do you expect the invalidation to change the page content?
- Switch back to the CloudFront viewer page in Safari.
- Refresh the page by pressing Cmd+R.
You will still see Release: v2 because the origin already contains that release.
The gradient background remains behind the white status card. Both green status badges remain visible.
You have now published an origin update and completed the matching CloudFront invalidation workflow.
Secret mission
Roll Back an Accidental S3 Overwrite
Protect your website from an accidental overwrite with S3 Versioning. Recover the earlier release as the newest object version without deleting its history.
Clean Up Your Resources
Clean Up Your Resources
This lab's emulated resources run on your Mac without creating AWS charges. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- A persistent LocalStack AWS Emulator container with persistence enabled.
- An S3 bucket named testwebsite with a public-read policy, versioned index.html history, and a style.css object.
- A CloudFront distribution with invalidation records for /*.
- Project files inside the s3-cloudfront-local folder.
Keep everything running
No action needed. Choose this if you want to keep testing the website through its recorded CloudFront domain.
- Leave the LocalStack emulator running with persistence enabled.
- Keep the s3-cloudfront-local folder for future changes.
- Continue using the recorded CloudFront domain to access Release: v2.
Pause - I'll come back to this later
Shut down the running emulator to free up local memory. Your persistent AWS state and project files remain available for later.
- Switch back to the terminal from earlier.
- Pause the LocalStack emulator by running this command:
lstk stop
What Does This Command Do?
The stop command shuts down the configured emulator. Your persistent state remains available on your Mac.
- Restart the persistent emulator from the s3-cloudfront-local folder when you return by running this command:
lstk start --persist
What Happens When You Return?
The start command launches the emulator with persistence enabled. Your S3 bucket and CloudFront distribution become available again.
Delete - I don't want to use this again
Remove all emulated AWS resources. Stop LocalStack after confirming the reset.
Deletion Is Permanent
This cleanup removes the lab's emulated S3 and CloudFront state. The final shell command also removes every file inside s3-cloudfront-local.
- Switch back to the terminal from earlier.
- Clear the emulated S3 and CloudFront resources by running this command:
lstk reset --force
What Does the Reset Remove?
The reset clears the deployed resources from the running AWS emulator. This includes the testwebsite bucket, its version history, the distribution, and its invalidation records.
- Confirm the emulator has no deployed resources by running this command:
lstk status
What Should You See?
You should see that the LocalStack AWS Emulator is still running with no deployed resources. This confirms the reset removed the lab's emulated state.
- Stop the empty emulator by running this command:
lstk stop
What Does the Stop Confirm?
You should see confirmation that the configured emulator stopped. It no longer uses memory as a running process.
- Remove the s3-cloudfront-local folder from its parent directory by running these commands:
cd ..
rm -rf s3-cloudfront-local
What Do These Commands Do?
The first command moves the terminal to the parent directory. The second command permanently deletes the s3-cloudfront-local folder and its contents.
- Confirm the project folder is gone by listing the remaining folders:
ls
What Should You See?
You should no longer see s3-cloudfront-local in the output. Your emulated resources and local project files are now cleaned up.
Nice Work!
Nice Work!
You did it! Your static website now travels from an S3 origin through a CloudFront viewer URL inside LocalStack.
You've learned how to:
- Turn each local website file into a separate S3 object key. Diagnose a missing stylesheet by comparing its page reference with the bucket contents.
- Deliver the site through an S3-backed CloudFront distribution. Trace each viewer request from the distribution to its origin object.
- Publish Release: v2 through the existing viewer URL. Submit a CloudFront invalidation while distinguishing LocalStack's immediate updates from real CloudFront caching.
- Complete the Secret Mission by enabling S3 Versioning. Restore an earlier release by copying its version in place without deleting the object history.
Ready to quiz yourself?