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.
| Problem | The function CloudFront solves it with |
|---|---|
| A US user fetching from Seoul S3 takes 200ms+ | ms responses with the Edge cache |
| HTTPS isn’t direct | ACM 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 hand | Automatic |
| WAF / Shield by hand | Integrated with CloudFront |
| DDoS | AWS Shield Standard automatic |
S3 + CloudFront is the standard pattern for all static sites.
The structure of CloudFront #
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 #
aws cloudfront create-distribution \
--origin-domain-name my-bucket.s3.ap-northeast-2.amazonaws.com \
--default-root-object index.htmlThe settings are as follows.
| Option | Meaning |
|---|---|
| Origin domain | An S3 bucket, an ALB DNS, or a direct domain |
| Default root object | The file to send on a / request (index.html) |
| Alternate domain (CNAME) | example.com, www.example.com |
| SSL certificate | ACM (must be us-east-1) |
| Price class | Up to which continent’s Edges to use |
| Logging | Store access logs in S3 |
| WAF | Attach a Web ACL |
Price Class #
| Price Class | Meaning |
|---|---|
| All | Every Edge worldwide — highest cost |
| 200 | US / Europe / Asia / Australia |
| 100 | US / 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.
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.
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.
Path Pattern | Origin | Cache Policy | TTL
---------------+--------+--------------+--------
/api/* | ALB | NoCaching | 0
/static/* | S3 | Optimized | 1 day
default (*) | S3 | Optimized | 1 hourThe 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.
CachingOptimized ← static assets (images / JS / CSS)
CachingOptimizedForUncompressedObjects ← uncompressed assets
CachingDisabled ← don't cache APIs
Elemental-MediaPackage ← media streamingThe 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
Query: ignore
Header: ignore (or Accept, Accept-Encoding)
Cookie: ignoreQuery: all
Header: Accept-Language, CloudFront-Is-Mobile-Viewer
Cookie: session-idOrigin Request Policy #
Sets what to send to the Origin. A different concept from the cache key.
| Cache Policy | Origin Request Policy | |
|---|---|---|
| Decides what | Cache key + TTL | Headers / query / cookies sent to the Origin |
| Effect | Cache hit rate | What 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.
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.
| TTL | Role |
|---|---|
| 0 | No caching. Every request goes to the Origin |
| 60 ~ 300 seconds | Frequently changing content (news headlines) |
| 1 hour ~ 1 day | General 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).
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 cacheThe 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.
user
│
▼
example.com (Route 53 Alias) ──▶ CloudFront
│
▼ (OAC)
S3 my-bucket ← all Public Access Block on
│
▼ Bucket Policy
allow GetObject to CloudFront onlyOAC — Origin Access Control #
The old way is OAI (Origin Access Identity). The new recommended way is 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/xreturns 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 method | A virtual IAM Identity of CloudFront | SigV4 signing |
| New regions / features | Partial support | All |
| KMS-encrypted S3 | Needs extra setup | Smooth |
| Recommendation | Review migration | The 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.
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.
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 automaticallyWith 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.
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.
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.
ACM (us-east-1)
├── *.example.com ← used by CloudFront
└── example.com
ACM (ap-northeast-2)
├── api.example.com ← used by the ALBWhen 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.comand the Distribution’s Alternate domain isapp.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=31536000as-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 toindex.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.htmlredirect not working — The S3 static site’s automatic conversion of/about/→/about/index.htmldoesn’t work in an OAC configuration. Do path rewrite yourself with a CloudFront 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 #
- 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.
- 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.
- 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.