EduFlow
AWS Cloud Foundations
0%

Section 4 · Building Something Real

Host a Static Site on S3 and CloudFront

17 min3 min readPut in order

Lesson at a glance

Ship Lantern’s front end from a private S3 bucket through Amazon CloudFront with origin access control, HTTPS on a custom domain, sensible cache headers and invalidations.

  1. 1The shape of it
  2. 2Step by step
  3. 3Cost and security in one glance

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Remember the street-food festival that took Lantern down? Most of those requests were for the same few files: the home page, a stylesheet, JavaScript and one poster. Lantern’s front end is a Vite app that builds to static files, so it doesn’t need a server at all. It needs a place to keep files, Amazon S3, and a global cache in front of it, Amazon CloudFront.

The shape of it

A browser requests lantern.example over HTTPS from a CloudFront edge location. On a cache hit the edge answers directly. On a miss, CloudFront signs a request with origin access control to the private S3 bucket, which allows only that distribution. Browser CloudFront edge cache S3 bucket private, BPA on HTTPS cache hit miss: signed Bucket policy: allow s3:GetObject only for this distribution
Viewers hit a nearby edge location; only cache misses reach the private bucket

Most requests are cache hits, answered by an edge location near the visitor without touching S3. On a miss, CloudFront fetches the file from the bucket, signing the request with origin access control (OAC). The bucket stays private, with Block Public Access on, and its policy allows reads from that one distribution only.

Step by step

1. Build and create a private bucket

Terminal
npm run build                      # Vite writes dist/
aws s3 mb s3://lantern-site-prod --region eu-west-1

The new bucket is private by default. Do not turn on static website hosting: OAC needs the bucket’s regular endpoint, and the website endpoint only speaks HTTP and requires public access.

2. Upload with the right cache headers

Vite puts a content hash in asset file names (assets/index-4f8a1c.js), so a changed file always gets a new name. Those can be cached for a year. index.html keeps its name, so browsers and CloudFront should always check for a newer one:

Terminal
aws s3 sync dist/ s3://lantern-site-prod/ --delete \
  --exclude "index.html" \
  --cache-control "public,max-age=31536000,immutable"

aws s3 cp dist/index.html s3://lantern-site-prod/index.html \
  --cache-control "no-cache"

--delete removes files from the bucket that no longer exist in dist/, so old bundles don’t pile up.

3. Get a certificate in us-east-1

For HTTPS on lantern.example, request a public certificate from AWS Certificate Manager in us-east-1. CloudFront only uses certificates from that Region, whatever Region your bucket is in. Validate it with the DNS record ACM gives you.

Terminal
aws acm request-certificate --region us-east-1 \
  --domain-name lantern.example --validation-method DNS

4. Create the distribution

In the CloudFront console, create a distribution with:

  • Origin: the bucket’s regular endpoint, with origin access control (sign requests).
  • Default root object: index.html.
  • Viewer protocol policy: redirect HTTP to HTTPS.
  • Alternate domain name lantern.example and the us-east-1 certificate.
  • Cache policy CachingOptimized, and the managed SecurityHeadersPolicy response headers policy, which adds HSTS and related headers.

If the front end uses client-side routes such as /events/street-food, those paths don’t exist as files, and S3 answers through OAC with 403. Add a custom error response that serves /index.html with status 200 for 403 errors, or a small CloudFront Function that rewrites those paths, so that deep links shared on social media open the right page instead of an error.

5. Let only that distribution read the bucket

site-bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::lantern-site-prod/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E2QWRUHEXAMPLE"
        }
      }
    }
  ]
}

The condition is what makes this safe: without it, any CloudFront distribution in any account could read the bucket.

6. Point DNS and test

Create alias A and AAAA records for lantern.example pointing to the distribution (in Amazon Route 53, or as a CNAME at another DNS host for a subdomain). Then check that https://lantern.example loads, and that the bucket’s own URL returns 403 Access Denied: that 403 proves the bucket is private.

7. On every deploy

Repeat step 2, then invalidate only the file that keeps its name:

Terminal
aws cloudfront create-invalidation \
  --distribution-id E2QWRUHEXAMPLE --paths "/index.html"

Cost and security in one glance

You pay S3 for storage and for requests, and with CloudFront in front, S3 only sees cache misses. Transfer from S3 to CloudFront is not charged; CloudFront bills for requests and data delivered to viewers on pay-as-you-go, or you can choose one of its flat-rate plans that bundle the CDN with AWS WAF, DNS and S3 credits for a fixed monthly price with no overage charges. Either way, the festival spike now lands on edge locations built for it.

Security-wise: one private bucket, one bucket policy with a SourceArn condition, HTTPS only, security headers on. Next you add the dynamic part: the RSVP API.

Key takeaways

  • The bucket stays private; CloudFront reads it through origin access control, and a bucket policy allows only that one distribution.
  • Origin access control works with the bucket’s regular endpoint, not the S3 static website endpoint.
  • A certificate for a CloudFront custom domain must live in AWS Certificate Manager in us-east-1, requested or imported there.
  • Give hashed asset files a long cache lifetime and index.html a short one, so deploys show up without mass invalidations.
  • Serving from edge locations absorbs traffic spikes and reduces data transfer from the origin.

Further reading

Check your understanding

3 questions

Question 1 of 3

Why does Lantern keep the site bucket private instead of turning on S3 static website hosting and making it public?

Question 2 of 3

You request a certificate for lantern.example in AWS Certificate Manager in eu-west-1, but CloudFront does not list it. Why?

Question 3 of 3

After a deploy, visitors still see the old version of the home page for hours, while new JavaScript files load fine. What is the most likely cause?

Practice

Put in order

Ship the static site

Put the steps for Lantern’s first production deploy of the front end in a working order. The bucket stays private and the site is served only through CloudFront on https://lantern.example. Finish the build-and-S3 work first, then do the certificate, CloudFront and DNS work.

Use the arrow buttons to reorder.

  1. 1Add the bucket policy that allows s3:GetObject for cloudfront.amazonaws.com with an AWS:SourceArn condition for this distribution.
  2. 2Create the CloudFront distribution with origin access control, index.html as default root object and the certificate attached.
  3. 3Request and DNS-validate an ACM certificate for lantern.example in us-east-1.
  4. 4Create alias A and AAAA records for lantern.example pointing at the distribution.
  5. 5Create the private lantern-site-prod bucket in Lantern’s Region, with Block Public Access left on.
  6. 6Upload dist/ with aws s3 sync, long cache headers for hashed assets and no-cache for index.html.
  7. 7Check that https://lantern.example loads and that the bucket’s direct URL returns 403.
  8. 8Run npm run build to produce the dist/ folder.