Steve Kinney

Exercise: End-to-End Static Site Deployment

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.html file. 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.html

Create 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 json

All 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 json

Refer to Uploading and Organizing Files for options like --cache-control headers.

Checkpoint

aws s3 ls s3://my-frontend-app-assets \
  --region us-east-1

You 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 json

The 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-certificate with --domain-name example.com and --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 SigningProtocol to sigv4, SigningBehavior to always, OriginAccessControlOriginType to s3.
  • Save the OAC Id from 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 CachingOptimized policy (658327ea-f89d-4fab-a63d-7e88639e58f6).
  • Custom error responses: Map 403 and 404 to /index.html with a 200 response code.
  • Viewer certificate: Your ACM certificate ARN, sni-only, TLSv1.2_2021.
  • Aliases: example.com and www.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-1

Then verify the CloudFront domain serves your site:

curl -I https://YOUR_CLOUDFRONT_DOMAIN/index.html

You 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.com service principal.
  • Action: s3:GetObject.
  • Resource: arn:aws:s3:::my-frontend-app-assets/*.
  • Condition: StringEquals on AWS:SourceArn matching 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.html

Direct S3 access should be blocked:

curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.html

Should 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.com pointing to your distribution.
  • Create an A alias record for www.example.com pointing 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.com

You 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.html

Checkpoint

  • dig example.com returns CloudFront IP addresses
  • curl -I https://example.com returns 200 OK
  • The SSL certificate subject matches your domain
  • curl -I https://example.com/any/spa/route returns 200 OK (SPA routing)
  • Direct S3 access returns 403 Forbidden
  • curl -I https://www.example.com also returns 200 OK

Test a Deployment

Deploy a change to verify the update cycle works:

  1. Edit your index.html (change the heading text).
  2. Sync the updated file to S3:
aws s3 sync ./build s3://my-frontend-app-assets \
  --region us-east-1 \
  --delete \
  --output json
  1. Invalidate the CloudFront cache:
aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/*" \
  --region us-east-1 \
  --output json
  1. 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

  • dig resolves the domain but curl -I https://example.com returns 403: 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" for index.html. Verify the headers appear in curl -I responses.

  • Security headers: Attach the managed SecurityHeadersPolicy (67f7725c-6f97-4210-82d7-5512b31e9d03) to your distribution’s default cache behavior. Verify strict-transport-security, x-content-type-options, and x-frame-options appear in the response headers.

  • Deploy script: Write a deploy.sh script 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.

Last modified on .