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.
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
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
npm run build # Vite writes dist/
aws s3 mb s3://lantern-site-prod --region eu-west-1The 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:
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.
aws acm request-certificate --region us-east-1 \
--domain-name lantern.example --validation-method DNS4. 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.exampleand 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
{
"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:
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.