Contents
14 Chapter

Deploying a static site with CloudFront

AWS's global CDN, CloudFront. The flow of Origin / Behavior / Cache Policy, the S3 + CloudFront static hosting pattern, how to safely shield S3 with OAC, and the operational flow of invalidation.

The static hosting method of Chapter 10 S3 has limits. It can’t do HTTPS, it can’t cache close to the user, and a custom domain and SSL don’t attach directly either. The service that solves these three in one go is CloudFront.

CloudFront is AWS’s CDN (Content Delivery Network). It places caches at over 600 Edge Locations worldwide, and the Edge nearest the user responds. It can pass through not only static sites but also dynamic APIs.

In this chapter we start from CloudFront’s structure and lay out the S3 + CloudFront pattern, OAC, cache policies, and invalidation at a glance. The ACM region rule used here is an extension of Chapter 13 ALB / NLB and ACM, and domain connection is the Alias of Chapter 12 Route 53. Part 2’s basic toolbox — EC2 / VPC / S3 / RDS / Route 53 / ALB / CloudFront — comes together in one place in this chapter.

The problem a CDN solves #

If you use an S3 static site as-is, the following aren’t good.

ProblemThe function CloudFront solves it with
A US user fetching from Seoul S3 takes 200ms+ms responses with the Edge cache
HTTPS isn’t directACM certificate + automatic HTTPS
Egress cost is expensive ($0.09/GB)CloudFront → internet ($0.085/GB) + cache hits are free
Cache headers / compression / HTTP/2 / HTTP/3 by handAutomatic
WAF / Shield by handIntegrated with CloudFront
DDoSAWS Shield Standard automatic

S3 + CloudFront is the standard pattern for all static sites.

The structure of CloudFront #

The structure of a CloudFront Distribution
 user
  │ HTTPS request: example.com/path/to/asset.js
┌────────────────────────────────────┐
│       CloudFront Edge Location      │
│       (the point nearest the user)  │
│                                      │
│    Cache check                       │
│      ├── HIT → respond from cache    │
│      └── MISS                        │
└────────────────────────────────────┘
                │ on MISS
        ┌────────────────────┐
        │   Cache Behavior   │
        │  (path match / policy) │
        └────────┬───────────┘
            ┌────────┐
            │ Origin │  ← S3, ALB, EC2, Lambda Function URL ...
            └────────┘

The core components are as follows.

  • Distribution — one bundle of settings (domain + certificate + behaviors).
  • Origin — where to fetch on a cache miss (S3, ALB, your domain, etc.).
  • Cache Behavior — path matching, the cache policy, and Origin matching.

Creating a Distribution #

A simple CloudFront Distribution
aws cloudfront create-distribution \
  --origin-domain-name my-bucket.s3.ap-northeast-2.amazonaws.com \
  --default-root-object index.html

The settings are as follows.

OptionMeaning
Origin domainAn S3 bucket, an ALB DNS, or a direct domain
Default root objectThe file to send on a / request (index.html)
Alternate domain (CNAME)example.com, www.example.com
SSL certificateACM (must be us-east-1)
Price classUp to which continent’s Edges to use
LoggingStore access logs in S3
WAFAttach a Web ACL

Price Class #

Price ClassMeaning
AllEvery Edge worldwide — highest cost
200US / Europe / Asia / Australia
100US / Europe / Israel / some of Asia — cheapest

For Korean / Japanese users only, 200 or higher is recommended. For a global service, All.

Origin — where to fetch from #

S3 Origin #

The most common case. Used for static sites / images / videos.

S3 Origin
Origin: my-bucket.s3.ap-northeast-2.amazonaws.com
        (REST API endpoint, NOT the s3-website-... URL)

Use the REST API URL, not the s3-website-* URL. That way you can protect it safely with OAC.

Custom Origin (ALB / an arbitrary HTTP server) #

An ALB / EC2 / external server can also be an Origin. This pattern is for accelerating dynamic APIs.

ALB Origin
Origin: my-alb-1234567890.elb.ap-northeast-2.amazonaws.com
HTTPS Port: 443
SSL Protocols: TLSv1.2, TLSv1.3
Origin Path: (none, or /api)

The ALB → CloudFront → user pattern plays the role of WAF + Edge SSL termination + caching.

Lambda Function URL Origin #

Lambda directly serves as the Origin. Used for serverless static + dynamic sites (Chapter 17 Lambda Basics).

Origin Group — failover #

When the primary Origin dies, it moves to the secondary. A multi-region DR configuration.

Cache Behavior — routing + cache policy #

Within the same Distribution, you can give a different behavior per path.

The role of Cache Behavior
Path Pattern   | Origin | Cache Policy | TTL
---------------+--------+--------------+--------
/api/*         | ALB    | NoCaching    | 0
/static/*      | S3     | Optimized    | 1 day
default (*)    | S3     | Optimized    | 1 hour

The processing order is path-pattern matching (the more specific first), and if there’s no match it goes to default.

Cache Policy #

Defines the cache behavior. There are default policies AWS provides and custom policies.

Default Cache Policies
CachingOptimized               ← static assets (images / JS / CSS)
CachingOptimizedForUncompressedObjects  ← uncompressed assets
CachingDisabled                ← don't cache APIs
Elemental-MediaPackage         ← media streaming

The cache key (what, when different, becomes a separate cache entry) is set by the following.

  • Query string — all / some / none
  • Header — which headers to distinguish by
  • Cookie — which cookies to distinguish by
The cache key of simple static assets
Query: ignore
Header: ignore (or Accept, Accept-Encoding)
Cookie: ignore
The cache key of language / device-split SSR
Query: all
Header: Accept-Language, CloudFront-Is-Mobile-Viewer
Cookie: session-id

Origin Request Policy #

Sets what to send to the Origin. A different concept from the cache key.

Cache PolicyOrigin Request Policy
Decides whatCache key + TTLHeaders / query / cookies sent to the Origin
EffectCache hit rateWhat influences the dynamic response

Response Headers Policy #

Automatically adds or modifies response headers. Used for security headers like Strict-Transport-Security, X-Content-Type-Options.

Recommended security headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; ...

TTL — how long to cache #

The criteria for cache TTL are as follows.

TTLRole
0No caching. Every request goes to the Origin
60 ~ 300 secondsFrequently changing content (news headlines)
1 hour ~ 1 dayGeneral static assets
1 year (max-age=31536000)hashed filename (app.abc123.js)

The Cache-Control header #

The Origin response’s Cache-Control header takes precedence (as long as it falls within Min/Default/Max TTL).

The Origin's Cache-Control response
Cache-Control: public, max-age=31536000, immutable   ← hashed asset (1 year)
Cache-Control: public, max-age=300                   ← HTML page (5 min)
Cache-Control: no-store                              ← never cache

The operational criteria are as follows.

  • HTML short (60s ~ 5min) — to show a new build quickly.
  • JS / CSS / images with hash 1 year + immutable — when the content changes, it becomes a new filename.
  • APIs usually not cached (or short).

The S3 + CloudFront pattern #

The standard setup for a static site.

The S3 + CloudFront standard pattern
user
example.com (Route 53 Alias) ──▶ CloudFront
                                    ▼ (OAC)
                                  S3 my-bucket  ← all Public Access Block on
                                    ▼ Bucket Policy
                                  allow GetObject to CloudFront only

OAC — Origin Access Control #

The old way is OAI (Origin Access Identity). The new recommended way is OAC.

The role of OAC
- All PAB on for S3 (never public)
- The Bucket Policy allows only CloudFront's OAC:

  {
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EXXXXXX"
        }
      }
    }]
  }

Doing this gives the following.

  • Direct S3 access is all blocked. Coming in via s3.amazonaws.com/my-bucket/x returns 403.
  • Only CloudFront passes through. It responds only after going through the Edge cache.
  • You get Egress cost savings, cache-hit acceleration, and WAF application all at once.

OAI vs OAC #

OAI (old way)OAC (new way)
Auth methodA virtual IAM Identity of CloudFrontSigV4 signing
New regions / featuresPartial supportAll
KMS-encrypted S3Needs extra setupSmooth
RecommendationReview migrationThe default for new setups

New setups use OAC, and the old way uses OAI then migrates. S3’s PAB and Bucket Policy follow the security evaluation order of Chapter 10 S3 as-is.

Invalidation #

The action of forcibly clearing cached objects before expiry. It makes a new version visible immediately right after deployment.

Creating an Invalidation
aws cloudfront create-invalidation \
  --distribution-id EXXXXXX \
  --paths "/*"

The ways to use it are as follows.

  • One line of /* after a static site deployment.
  • Refreshing only specific pages: /index.html /about.html.
  • A wildcard: /blog/2026/*.

The cost trap #

CloudFront’s invalidation is free for the first 1000 paths per month, after which it’s $0.005 per path. One /* is 1 path, so it’s fine to call it frequently.

Cache Versioning pattern — not using invalidation #

In real operations you don’t use invalidation. Instead you use the following pattern.

The hashed filename pattern
build output:
  /static/app.abc123.js   ← content hash
  /static/app.def456.js   ← the next build is a different hash

HTML cached short (1 min), JS/CSS 1 year:
  Cache-Control: public, max-age=31536000, immutable

on a new deployment:
  /index.html points to the new-hash JS
  HTML cache expires (1 min) → the new JS loads automatically

With this pattern you reflect changes instantly without invalidation while keeping cache efficiency at maximum.

Lambda@Edge / CloudFront Functions #

Their role is to intercept requests / responses at the Edge and run code.

CloudFront Functions #

  • JavaScript ES5 only.
  • Handles only the viewer stage of request / response.
  • Within 5ms, within 1MB memory.
  • Very cheap ($0.10 / million).
  • Uses are redirects, adding headers, A/B testing, auth token checks.
A simple redirect
function handler(event) {
  var request = event.request;
  if (request.uri === '/old-page') {
    return {
      statusCode: 301,
      statusDescription: 'Moved Permanently',
      headers: { 'location': { value: '/new-page' } }
    };
  }
  return request;
}

Lambda@Edge #

  • Uses Node.js / Python.
  • Handles four stages (viewer request / origin request / origin response / viewer response).
  • A limit of 5 seconds (viewer) / 30 seconds (origin).
  • Expensive and slow but powerful.
  • Uses are dynamic content transformation, multi-region routing, complex authentication.

These days you start with CloudFront Functions as much as possible, and use Lambda@Edge only when that falls short.

Signed URL / Signed Cookie — private content #

Used for cases like paid videos or exclusive downloads. CloudFront’s signature limits time and IP.

The shape of a Signed URL
https://d111111abcdef8.cloudfront.net/video.mp4?
  Expires=1714992000&
  Signature=...
  Key-Pair-Id=APKAEX...
  • Signed URL — one URL for one resource.
  • Signed Cookie — one user accesses multiple resources (a site-level subscription).

It’s different from S3’s presigned URL. Because it’s verified at the CloudFront Edge, it’s more global and cacheable.

CloudFront and ACM’s region rule #

ACM’s region rule covered in Chapter 13 ALB / NLB and ACM.

A CloudFront certificate is always us-east-1
ACM (us-east-1)
  ├── *.example.com         ← used by CloudFront
  └── example.com

ACM (ap-northeast-2)
  ├── api.example.com       ← used by the ALB

When you receive a certificate in the console, you must switch the region to N. Virginia (us-east-1).

Compression and HTTP/2 / HTTP/3 #

CloudFront handles these automatically.

  • gzip / Brotli compression — turn on the option.
  • HTTP/2 — default.
  • HTTP/3 (QUIC) — an option.
  • TLS 1.3 — set in the Security Policy.

This handling is smoother through CloudFront than exposing the ALB directly.

Common pitfalls #

  • S3 still Public — The case where you set up OAC but didn’t turn on PAB, so S3 is directly accessible. Turn on all four PAB and make the Bucket Policy allow only CloudFront.
  • CNAME mismatch — If the ACM certificate is *.example.com and the Distribution’s Alternate domain is app.example.com (OK since it’s a wildcard child) it’s fine, but if you plug it in wrong, an SSL certificate mismatch error occurs. Match the certificate’s SAN and the domain exactly.
  • The cache too strong, so a new deployment isn’t visible — If you cache HTML with max-age=31536000 as-is, users receive the old HTML even after a new deployment. Keep HTML short and only hashed assets long.
  • A wrong query cache key — If you put all query strings in the cache key, the same file becomes a separate cache for every ?v=1, ?v=2, making the hit rate 0%. Put only the queries that genuinely have an effect in the cache key.
  • Missing the Default root object — A request to / returns 403/404. Set the Default root object to index.html.
  • The Host header sent to the Origin — For an S3 Origin, CloudFront handles the Host header automatically. But for an ALB or arbitrary server, specify the Host-header forwarding option in the Origin Request Policy.
  • The Origin’s SSL certificate not trusted — If a Custom Origin (your own server) is self-signed, CloudFront rejects it. Use an ACM-issued certificate or a trusted CA.
  • index.html redirect not working — The S3 static site’s automatic conversion of /about//about/index.html doesn’t work in an OAC configuration. Do path rewrite yourself with a CloudFront Function.
path rewrite Function
function handler(event) {
  var request = event.request;
  var uri = request.uri;
  if (uri.endsWith('/')) {
    request.uri = uri + 'index.html';
  } else if (!uri.includes('.')) {
    request.uri = uri + '/index.html';
  }
  return request;
}

Exercises #

  1. Without looking at the §“The S3 + CloudFront pattern” diagram, write down, in order from the Chapter 12 Route 53 Alias through OAC to the Bucket Policy, the path by which a user request reaches S3 from the domain. Then explain in one sentence why the public read policy you turned on in the static hosting of Chapter 10 S3 is, conversely, disabled here.
  2. A report comes in that users still see the old screen even after a new deployment. Based on §“Cache Versioning pattern”, write down concretely what cache settings (the TTLs of HTML and hashed assets) you should set, instead of calling invalidation every time.
  3. Write down why you must not issue a CloudFront certificate in Seoul, based on §“CloudFront and ACM’s region rule”, and connect how this matches the region mapping table of Chapter 13 ALB / NLB and ACM.

In short: CloudFront is a global CDN that responds with caches at over 600 edges, composed of Distribution + Origin + Cache Behavior. S3 + CloudFront + OAC is the standard static site pattern, where you turn on all four of S3’s PAB settings and allow only CloudFront with the Bucket Policy. The TTL standard is short for HTML and one year for hashed assets, so changes reflect instantly without invalidation, and a CloudFront certificate is always issued in us-east-1.

Next chapter #

With this, Part 2’s basic toolbox — EC2 / VPC / S3 / RDS / Route 53 / ALB / CloudFront — has come together in one place. From Chapter 15 ECS and Fargate, which starts Part 3, we add the container / serverless / event-driven domains on top. We move the way of running apps directly on EC2 to containers, and lay out the operation of ECS / Fargate, the container version of the ASG.

X