You’re going to deploy a static site from zero to production. No shortcuts, no skipping steps. This is my favorite exercise in the whole course. By the end, your site will be live on a *.cloudfront.net domain—served through CloudFront, secured with HTTPS, and stored in a private S3 bucket. This exercise integrates the core static-hosting arc: IAM, S3, CloudFront, and OAC.
Why It Matters
You’ve built each piece individually across the early static-hosting sections. This exercise proves they compose into a working deployment. It’s also a dry run for the workflow you’ll automate with GitHub Actions: every manual step here maps to a step in your CI/CD pipeline. If you can do it by hand, you understand what the automation is doing.
If you want the AWS version of the end-to-end workflow open while you work, keep the S3 static website tutorial and the CloudFront OAC guide nearby.
Prerequisites
Before you start:
- AWS CLI v2 configured with credentials that have admin-level permissions (or at minimum: S3, CloudFront, and IAM access). See Setting Up the AWS CLI.
- A static site build directory with at least an
index.htmlfile. If you don’t have one, create a minimal site:
mkdir -p build
echo '<!DOCTYPE html><html><head><title>My Site</title></head><body><h1>It works.</h1></body></html>' > build/index.htmlCreate the S3 Bucket
Create a bucket to hold your static files. Block all public access from the start—CloudFront will be the only way to reach these files.
- Create the bucket with
aws s3 mb. - Enable Block Public Access with
aws s3api put-public-access-block. - Verify the bucket exists with
aws s3 ls.
Refer to Creating and Configuring a Bucket for the commands.
Checkpoint
aws s3api get-public-access-block \
--bucket my-frontend-app-assets \
--region us-east-1 \
--output jsonAll four settings should be true.
Upload Your Site
Sync your build directory to the bucket:
aws s3 sync ./build s3://my-frontend-app-assets \
--region us-east-1 \
--output jsonRefer to Uploading and Organizing Files for options like --cache-control headers.
Checkpoint
aws s3 ls s3://my-frontend-app-assets \
--region us-east-1You should see your index.html (and any other files) listed.
Create an Origin Access Control
Create an OAC that CloudFront will use to authenticate requests to your S3 bucket:
- Use
aws cloudfront create-origin-access-control. - Set
SigningProtocoltosigv4,SigningBehaviortoalways,OriginAccessControlOriginTypetos3. - Save the OAC
Idfrom the response.
Refer to Origin Access Control for S3.
Checkpoint
aws cloudfront list-origin-access-controls \
--region us-east-1 \
--output json \
--query "OriginAccessControlList.Items[*].{Id:Id,Name:Name}"Your OAC should appear in the list.
Create the CloudFront Distribution
Create a distribution that ties the bucket, OAC, and certificate together. Your distribution config should include:
- Origin: Your S3 bucket with the OAC ID attached.
- Default root object:
index.html. - Viewer protocol policy:
redirect-to-https. - Cache policy: The managed
CachingOptimizedpolicy (658327ea-f89d-4fab-a63d-7e88639e58f6). - Custom error responses: Map 403 and 404 to
/index.htmlwith a 200 response code. - Viewer certificate: Use the CloudFront default certificate (
CloudFrontDefaultCertificate: true).
Refer to Creating a CloudFront Distribution and Custom Error Pages and SPA Routing.
Save the distribution ID and domain name from the response.
Checkpoint
Wait for deployment:
aws cloudfront wait distribution-deployed \
--id YOUR_DISTRIBUTION_ID \
--region us-east-1Then verify the CloudFront domain serves your site:
curl -I https://YOUR_CLOUDFRONT_DOMAIN/index.htmlYou should get 200 OK.
Update the S3 Bucket Policy
Replace the bucket policy to allow only CloudFront to read from the bucket:
- Principal:
cloudfront.amazonaws.comservice principal. - Action:
s3:GetObject. - Resource:
arn:aws:s3:::my-frontend-app-assets/*. - Condition:
StringEqualsonAWS:SourceArnmatching your distribution’s ARN.
Refer to Origin Access Control for S3 and Bucket Policies and Public Access.
Checkpoint
CloudFront should still work:
curl -I https://YOUR_CLOUDFRONT_DOMAIN/index.htmlDirect S3 access should be blocked:
curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.htmlShould return 403 Forbidden.
Verify the Complete Pipeline
Run through every layer of the pipeline:
# HTTPS works on the CloudFront domain
curl -I https://YOUR_CLOUDFRONT_DOMAIN
# SPA routing works (returns index.html for non-existent paths)
curl -I https://YOUR_CLOUDFRONT_DOMAIN/any/spa/route
# Direct S3 access is blocked
curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.htmlCheckpoint
-
curl -I https://YOUR_CLOUDFRONT_DOMAINreturns200 OK -
curl -I https://YOUR_CLOUDFRONT_DOMAIN/any/spa/routereturns200 OK(SPA routing) - Direct S3 access returns
403 Forbidden
Test a Deployment
Deploy a change to verify the update cycle works:
- Edit your
index.html(change the heading text). - Sync the updated file to S3:
aws s3 sync ./build s3://my-frontend-app-assets \
--region us-east-1 \
--delete \
--output json- Invalidate the CloudFront cache:
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*" \
--region us-east-1 \
--output json- Wait a minute, then reload
https://YOUR_CLOUDFRONT_DOMAIN. You should see the updated content.
Checkpoint
Your updated content is live at https://YOUR_CLOUDFRONT_DOMAIN. The deployment cycle (sync + invalidate) works end to end.
Failure Diagnosis
- CloudFront returns
403 Forbiddenfor files that exist: The S3 bucket policy does not trust your distribution’s Origin Access Control, or the origin is still configured incorrectly. - The updated page never appears after sync: The new file reached S3, but CloudFront is still serving the cached version. Confirm the invalidation completed before retesting.
Stretch Goals
Differentiated cache headers: Re-upload your assets with
--cache-control "public, max-age=31536000, immutable"for hashed files and--cache-control "public, max-age=60"forindex.html. Verify the headers appear incurl -Iresponses.Security headers: Attach the managed
SecurityHeadersPolicy(67f7725c-6f97-4210-82d7-5512b31e9d03) to your distribution’s default cache behavior. Verifystrict-transport-security,x-content-type-options, andx-frame-optionsappear in the response headers.Deploy script: Write a
deploy.shscript that automates the sync and invalidation steps. Refer to Automating Deploys with the AWS CLI for the template.
Want to put this behind a custom domain with HTTPS on your own certificate? See the optional Custom Domains, DNS, and Certificates section at the end of the course. It walks through Route 53, ACM, and wiring everything together.
When you’re ready, check your work against the Solution: End-to-End Static Site Deployment.