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 at https://example.com—served through CloudFront, secured with an ACM certificate, stored in a private S3 bucket, resolved by Route 53. This exercise integrates the entire early static-hosting arc: IAM, S3, domain control, ACM, CloudFront, and final DNS routing.
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, the CloudFront OAC guide, and the Route 53 DNS configuration guide nearby.
Prerequisites
Before you start:
- AWS CLI v2 configured with credentials that have admin-level permissions (or at minimum: S3, CloudFront, ACM, Route 53, and IAM access). See Setting Up the AWS CLI.
- A domain name you control, either registered through Route 53 or with nameservers pointed at a Route 53 hosted zone. See Registering and Transferring Domains.
- 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.
Confirm Domain and DNS Control
Before ACM can issue anything, make sure the domain is actually routed the way you expect:
- Confirm you control the registrar account for the domain.
- Confirm the domain either uses Route 53 as the registrar or already delegates DNS to a Route 53 hosted zone.
- Confirm the hosted zone exists in Route 53.
Refer to Registering and Transferring Domains and Hosted Zones and Record Types.
Checkpoint
aws route53 list-hosted-zones-by-name \
--dns-name example.com \
--output jsonThe output should include your hosted zone for example.com.
Request an ACM Certificate
Request a certificate in us-east-1 for your domain. Include both the apex domain and the www subdomain:
- Use
aws acm request-certificatewith--domain-name example.comand--subject-alternative-names www.example.com. - Use DNS validation (not email validation).
- Create the validation CNAME records in Route 53.
- Wait for the certificate status to become
ISSUED.
Refer to Requesting a Certificate in ACM and DNS Validation vs. Email Validation.
Checkpoint
aws acm describe-certificate \
--certificate-arn YOUR_CERTIFICATE_ARN \
--region us-east-1 \
--output json \
--query "Certificate.Status"This should return "ISSUED". If it still says "PENDING_VALIDATION", verify your DNS validation records are correct and wait a few minutes.
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: Your ACM certificate ARN,
sni-only,TLSv1.2_2021. - Aliases:
example.comandwww.example.com.
Refer to Creating a CloudFront Distribution, Attaching an SSL Certificate, 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.
Create Route 53 DNS Records
Create A alias records that point your domain to the CloudFront distribution:
- Create an A alias record for
example.compointing to your distribution. - Create an A alias record for
www.example.compointing to the same distribution.
Refer to Alias Records vs. CNAME Records and Pointing a Domain to CloudFront.
Checkpoint
After DNS propagates (this can take a few minutes, or up to 48 hours if you recently changed nameservers):
curl -I https://example.comYou should get 200 OK with your site’s content served over HTTPS.
If DNS hasn’t propagated yet, you can verify the distribution directly using the CloudFront domain name (d1234abcdef.cloudfront.net). Once DNS resolves, the custom domain will produce the same result.
Verify the Complete Pipeline
Run through every layer of the pipeline:
# DNS resolves to CloudFront
dig example.com +short
# HTTPS works with your certificate
curl -vI https://example.com 2>&1 | grep "subject:"
# CloudFront serves your content
curl -I https://example.com
# SPA routing works (returns index.html for non-existent paths)
curl -I https://example.com/any/spa/route
# Direct S3 access is blocked
curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.htmlCheckpoint
-
dig example.comreturns CloudFront IP addresses -
curl -I https://example.comreturns200 OK - The SSL certificate subject matches your domain
-
curl -I https://example.com/any/spa/routereturns200 OK(SPA routing) - Direct S3 access returns
403 Forbidden -
curl -I https://www.example.comalso returns200 OK
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://example.com. You should see the updated content.
Checkpoint
Your updated content is live at https://example.com. The deployment cycle (sync + invalidate) works end to end.
Failure Diagnosis
digresolves the domain butcurl -I https://example.comreturns403: DNS is fine. The problem is lower in the stack, usually an S3 bucket policy or Origin Access Control mismatch.- HTTPS works on the CloudFront domain but not on your custom domain: The Route 53 alias records or the alternate domain names on the distribution are incomplete, or the wrong ACM certificate is attached.
- 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.
When you’re ready, check your work against the Solution: End-to-End Static Site Deployment.