AWS Intermediate #7: CloudFront for static site delivery
In #3 S3 static hosting we saw a few limits — no HTTPS, no edge cache near users, no custom domain + SSL. The service that solves all three at once is CloudFront.
CloudFront is AWS’s CDN (Content Delivery Network). With caches in 600+ Edge Locations worldwide, the closest Edge to the user responds. It’s versatile — not just for static sites but also for proxying dynamic API traffic.
In this post we thread CloudFront’s shape → the S3 + CloudFront pattern → OAC → cache policies → invalidation.
What a CDN solves #
S3 static sites alone are weak in these areas:
| Problem | What CloudFront solves |
|---|---|
| US user fetching from Seoul S3 takes 200ms+ | Edge cache responds in ms |
| HTTPS isn’t direct | ACM cert + auto HTTPS |
| Egress is expensive ($0.09/GB) | CloudFront → internet ($0.085/GB) + cache hits free |
| Cache headers / compression / HTTP/2 / HTTP/3 | Auto |
| WAF / Shield direct | Integrated with CloudFront |
| DDoS | AWS Shield Standard auto |
S3 + CloudFront is the standard pattern for every static site.
Shape of CloudFront #
user
│
│ HTTPS request: example.com/path/to/asset.js
▼
┌────────────────────────────────────┐
│ CloudFront Edge Location │
│ (closest to the user) │
│ │
│ Cache check │
│ ├── HIT → serve from cache │
│ └── MISS │
└────────────────────────────────────┘
│ on MISS
▼
┌────────────────────┐
│ Cache Behavior │
│ (path match / pol) │
└────────┬───────────┘
▼
┌────────┐
│ Origin │ ← S3, ALB, EC2, Lambda Function URL ...
└────────┘Three core parts:
- Distribution — one bundle of settings (domain + cert + behaviors)
- Origin — where to fetch on cache miss (S3, ALB, custom domain, …)
- Cache Behavior — path match + cache policy + Origin match
Creating a Distribution #
aws cloudfront create-distribution \
--origin-domain-name my-bucket.s3.ap-northeast-2.amazonaws.com \
--default-root-object index.htmlSettings:
| Option | Meaning |
|---|---|
| Origin domain | S3 bucket, ALB DNS, or a custom domain |
| Default root object | File for / requests (index.html) |
| Alternate domain (CNAME) | example.com, www.example.com |
| SSL certificate | ACM (must be us-east-1) |
| Price class | Which continents to use Edges in |
| Logging | Save access logs to S3 |
| WAF | Attach Web ACL |
Price Class #
| Price Class | Meaning |
|---|---|
| All | All Edges worldwide — highest cost |
| 200 | US / Europe / Asia / Australia |
| 100 | US / Europe / Israel / parts of Asia — cheapest |
For audiences in Korea and Japan only, 200 or above is recommended. Global services use All.
Origin — where to fetch from #
S3 Origin #
The most common use. 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. Otherwise OAC (below) can’t protect it safely.
Custom Origin (ALB / arbitrary HTTP server) #
ALB / EC2 / external servers also work as Origin. This pattern is for dynamic API acceleration.
Origin: my-alb-1234567890.elb.ap-northeast-2.amazonaws.com
HTTPS Port: 443
SSL Protocols: TLSv1.2, TLSv1.3
Origin Path: (none or /api)Routing through CloudFront → ALB → user gives you WAF + Edge SSL termination + caching all at once.
Lambda Function URL Origin #
Lambda directly as Origin. Serverless static + dynamic site.
Origin Group — failover #
Fail over to Secondary if Primary Origin dies. For multi-region DR.
Cache Behavior — routing + cache policy #
Within the same Distribution, you can have different behavior per path.
Path Pattern | Origin | Cache Policy | TTL
---------------+--------+--------------+--------
/api/* | ALB | NoCaching | 0
/static/* | S3 | Optimized | 1 day
default (*) | S3 | Optimized | 1 hourOrder: path-pattern match (more specific first) → default if none.
Cache Policy #
Defines cache behavior. AWS-provided defaults + custom.
CachingOptimized ← static assets (images / JS / CSS)
CachingOptimizedForUncompressedObjects ← uncompressed assets
CachingDisabled ← API, no cache
Elemental-MediaPackage ← media streamingCache key parts (= what makes it a separate cache entry):
- Query string — all / some / none
- Header — which header(s) distinguish
- Cookie — which cookie(s) distinguish
Query: ignore
Header: ignore (or Accept, Accept-Encoding)
Cookie: ignoreQuery: all
Header: Accept-Language, CloudFront-Is-Mobile-Viewer
Cookie: session-idOrigin Request Policy #
Controls what to send to Origin. Different from the cache key.
| Cache Policy | Origin Request Policy | |
|---|---|---|
| What it decides | Cache key + TTL | Headers / queries / cookies sent to Origin |
| Effect | Cache hit rate | What can affect dynamic responses |
Response Headers Policy #
Adds / modifies response headers. Used for security headers like Strict-Transport-Security and 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 #
Cache TTL guide:
| TTL | Use |
|---|---|
| 0 | No cache. Every request hits Origin |
| 60–300 sec | Frequently-changing content (news headlines) |
| 1 hour – 1 day | General static assets |
1 year (max-age=31536000) | Hashed filename (app.abc123.js) |
Cache-Control header #
Origin’s Cache-Control header takes priority (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 cacheOperational guide:
- HTML = short (60s – 5min) — new builds visible quickly
- Hashed JS / CSS / images = 1 year + immutable — content changes get a new filename
- API = usually no cache (or short)
S3 + CloudFront pattern #
The standard static-site setup.
user
│
▼
example.com (Route 53 Alias) ──▶ CloudFront
│
▼ (OAC)
S3 my-bucket ← all Public Access Block on
│
▼ Bucket Policy
only CloudFront allowed GetObjectOAC — Origin Access Control #
The legacy approach is OAI (Origin Access Identity). The new recommended one is OAC.
- All PAB on for the S3 bucket (never public)
- 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"
}
}
}]
}Result:
- Direct S3 access blocked —
s3.amazonaws.com/my-bucket/xreturns 403 - Only CloudFront passes through — must go through Edge cache to respond
- Egress savings + cache hit acceleration + WAF
OAI vs OAC #
| OAI (legacy) | OAC (new) | |
|---|---|---|
| Auth method | CloudFront’s virtual IAM Identity | SigV4 signature |
| New regions / features | Partial | All |
| KMS-encrypted S3 | Extra config needed | Smooth |
| Recommended | Consider migration | Default for new projects |
New setups go OAC, legacy keeps OAI and migrates over time.
Invalidation #
A way to forcibly clear cached objects before expiry. Show new versions immediately after deploy.
aws cloudfront create-invalidation \
--distribution-id EXXXXXX \
--paths "/*"Usage:
- One line
/*after a static-site deploy - Specific pages only:
/index.html /about.html - Wildcards:
/blog/2026/*
Cost trap #
CloudFront invalidations are first 1000 paths / month free, then $0.005 per path. /* once = 1 path, so calling it often is fine.
Cache versioning pattern — avoid invalidation #
Real production doesn’t use invalidation. Instead:
Build outputs:
/static/app.abc123.js ← content hash
/static/app.def456.js ← next build, different hash
Cache HTML short (1 min), JS/CSS 1 year:
Cache-Control: public, max-age=31536000, immutable
On new deploy:
/index.html points to the new-hash JS
HTML cache expires (1 min) → new JS loads automaticallyThis pattern reflects instantly without invalidation + maximizes cache efficiency.
Lambda@Edge / CloudFront Functions #
Run code at the Edge to intercept requests / responses.
CloudFront Functions #
- JavaScript ES5 only
- Viewer request / response stages only
- Within 5ms, 1MB memory
- Very cheap ($0.10 / million)
- Uses: redirect, header injection, A/B testing, auth-token check
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 #
- Node.js / Python
- 4 stages (viewer request / origin request / origin response / viewer response)
- 5 sec (viewer) / 30 sec (origin)
- More expensive and slower, but powerful
- Uses: dynamic content transformation, multi-region routing, complex auth
Start with CloudFront Functions whenever possible and only escalate to Lambda@Edge when the extra capability is required.
Signed URL / Signed Cookie — private content #
For paid videos / restricted downloads. CloudFront’s signature limits time / IP.
https://d111111abcdef8.cloudfront.net/video.mp4?
Expires=1714992000&
Signature=...
Key-Pair-Id=APKAEX...- Signed URL — one URL per resource
- Signed Cookie — one user accessing many resources (subscription site)
Unlike S3’s presigned URL, CloudFront signatures are verified at the Edge — making them more globally distributed and cache-friendly.
CloudFront and ACM #
The region rule from #6:
ACM (us-east-1)
├── *.example.com ← used by CloudFront
└── example.com
ACM (ap-northeast-2)
├── api.example.com ← used by ALBWhen requesting a cert in the console, switch the region to N. Virginia (us-east-1).
Compression and HTTP/2 / HTTP/3 #
CloudFront automatically does:
- gzip / Brotli compression — toggle on
- HTTP/2 — default
- HTTP/3 (QUIC) — option
- TLS 1.3 — via Security Policy
This is smoother than exposing ALB directly.
Common pitfalls #
1) S3 still public #
OAC set up but PAB off → S3 directly reachable. Turn on all 4 PAB + Bucket Policy that only allows CloudFront.
2) CNAME mismatch #
ACM cert is *.example.com but Distribution Alternate domain is app.example.com (wildcard child, OK) — wrong setting yields SSL mismatch errors. Match cert SAN and the domain exactly.
3) Cache too strong, new deploy invisible #
max-age=31536000 cached HTML → users see old HTML after new deploy. Short cache for HTML / long only for hashed assets.
4) Wrong query cache key #
Caching every query → same file but ?v=1, ?v=2 cache separately → 0% hit rate. Only include query params that actually affect the response.
5) Missing default root object #
/ requests yield 403/404. Set Default root object = index.html.
6) Host header sent to Origin #
For S3 Origin, CloudFront handles Host automatically. But for ALB / custom servers, explicitly forward the Host header in the Origin Request Policy.
7) Origin SSL cert untrusted #
Custom Origin (your own server) with self-signed → CloudFront rejects. Use ACM-issued or trusted CA.
8) index.html redirect
#
S3 static-site /about/ → /about/index.html auto-rewrite doesn’t happen with OAC. Use a CloudFront Function for path rewrite:
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;
}Wrap-up #
What we took home this time:
- CloudFront = global CDN. Edges in 600+ places respond from cache
- Distribution + Origin + Cache Behavior trinity
- Origin = S3 / ALB / Custom HTTP / Lambda URL / Origin Group
- S3 + CloudFront + OAC is the standard static-site pattern
- OAC is the new recommended path. All 4 PAB on, Bucket Policy allows only CloudFront
- Cache Policy for cache key (query/header/cookie) + TTL. Origin Request Policy for what to send to Origin
- TTL strategy — short HTML, long + immutable for hashed assets
- Invalidation sometimes. Hashed filename pattern is the way to skip it
- CloudFront Functions = fast and cheap. Lambda@Edge = powerful but heavy
- Signed URL / Cookie for private content
- ACM cert must be
us-east-1 - Compression / HTTP/2 / HTTP/3 automatic
- Pitfalls — S3 public, CNAME mismatch, cache too strong, wrong query key, default root, Host header, Origin SSL, path rewrite
Next — Starting AWS Advanced #
That wraps up AWS Intermediate 7 posts. EC2 / VPC / S3 / RDS / Route 53 / ALB / CloudFront — the basic toolbox is gathered in one place.
Now we’ll add containers / serverless / event-driven pieces on top. In AWS Advanced #1 ECS and Fargate we move from launching apps directly on EC2 to containers, and lay out the operational side of ECS / Fargate — the container-native version of ASG.