2026-01-27 17:35:45 -05:00
|
|
|
---
|
|
|
|
|
name: cdn-setup
|
|
|
|
|
description: Configure CDNs for content delivery. Set up CloudFront, Cloudflare, and Fastly. Use when optimizing global content delivery.
|
|
|
|
|
license: MIT
|
|
|
|
|
metadata:
|
|
|
|
|
author: devops-skills
|
|
|
|
|
version: "1.0"
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# CDN Setup
|
|
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
Configure content delivery networks for fast, reliable global asset delivery with proper caching, invalidation, and security.
|
|
|
|
|
|
|
|
|
|
## When to Use
|
|
|
|
|
|
|
|
|
|
- Serving static assets (JS, CSS, images, fonts) globally with low latency.
|
|
|
|
|
- Offloading traffic from origin servers to reduce compute costs.
|
|
|
|
|
- Adding TLS termination and DDoS protection at the edge.
|
|
|
|
|
- Implementing geo-based routing or content restrictions.
|
|
|
|
|
- Accelerating API responses with edge caching.
|
|
|
|
|
|
|
|
|
|
## Prerequisites
|
|
|
|
|
|
|
|
|
|
- Domain with DNS management access.
|
|
|
|
|
- Origin server or S3/R2 bucket with content to serve.
|
|
|
|
|
- AWS CLI configured (for CloudFront).
|
|
|
|
|
- Cloudflare account with zone configured (for Cloudflare CDN).
|
|
|
|
|
- Terraform 1.5+ (for infrastructure-as-code examples).
|
2026-01-27 17:35:45 -05:00
|
|
|
|
|
|
|
|
## AWS CloudFront
|
|
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
### Create a Distribution via CLI
|
|
|
|
|
|
2026-01-27 17:35:45 -05:00
|
|
|
```bash
|
2026-03-24 18:02:50 -04:00
|
|
|
# Create an S3 origin distribution with OAC (Origin Access Control)
|
2026-01-27 17:35:45 -05:00
|
|
|
aws cloudfront create-distribution --distribution-config '{
|
2026-03-24 18:02:50 -04:00
|
|
|
"CallerReference": "my-site-'$(date +%s)'",
|
|
|
|
|
"Comment": "Production site CDN",
|
|
|
|
|
"Enabled": true,
|
2026-01-27 17:35:45 -05:00
|
|
|
"Origins": {
|
|
|
|
|
"Quantity": 1,
|
|
|
|
|
"Items": [{
|
2026-03-24 18:02:50 -04:00
|
|
|
"Id": "s3-origin",
|
|
|
|
|
"DomainName": "my-bucket.s3.us-east-1.amazonaws.com",
|
|
|
|
|
"OriginPath": "",
|
|
|
|
|
"S3OriginConfig": {
|
|
|
|
|
"OriginAccessIdentity": ""
|
|
|
|
|
},
|
|
|
|
|
"OriginAccessControlId": "E2QWRUHAPOMQZL"
|
2026-01-27 17:35:45 -05:00
|
|
|
}]
|
|
|
|
|
},
|
|
|
|
|
"DefaultCacheBehavior": {
|
2026-03-24 18:02:50 -04:00
|
|
|
"TargetOriginId": "s3-origin",
|
2026-01-27 17:35:45 -05:00
|
|
|
"ViewerProtocolPolicy": "redirect-to-https",
|
2026-03-24 18:02:50 -04:00
|
|
|
"AllowedMethods": {
|
|
|
|
|
"Quantity": 2,
|
|
|
|
|
"Items": ["GET", "HEAD"]
|
|
|
|
|
},
|
|
|
|
|
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
|
|
|
|
|
"Compress": true
|
2026-01-27 17:35:45 -05:00
|
|
|
},
|
2026-03-24 18:02:50 -04:00
|
|
|
"DefaultRootObject": "index.html",
|
|
|
|
|
"PriceClass": "PriceClass_100",
|
|
|
|
|
"ViewerCertificate": {
|
|
|
|
|
"ACMCertificateArn": "arn:aws:acm:us-east-1:123456789:certificate/abc-123",
|
|
|
|
|
"SSLSupportMethod": "sni-only",
|
|
|
|
|
"MinimumProtocolVersion": "TLSv1.2_2021"
|
|
|
|
|
},
|
|
|
|
|
"Aliases": {
|
|
|
|
|
"Quantity": 1,
|
|
|
|
|
"Items": ["www.example.com"]
|
|
|
|
|
},
|
|
|
|
|
"CustomErrorResponses": {
|
|
|
|
|
"Quantity": 1,
|
|
|
|
|
"Items": [{
|
|
|
|
|
"ErrorCode": 404,
|
|
|
|
|
"ResponseCode": "200",
|
|
|
|
|
"ResponsePagePath": "/index.html",
|
|
|
|
|
"ErrorCachingMinTTL": 10
|
|
|
|
|
}]
|
|
|
|
|
}
|
2026-01-27 17:35:45 -05:00
|
|
|
}'
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
### Cache Invalidation
|
2026-01-27 17:35:45 -05:00
|
|
|
|
|
|
|
|
```bash
|
2026-03-24 18:02:50 -04:00
|
|
|
# Invalidate specific paths
|
|
|
|
|
aws cloudfront create-invalidation \
|
|
|
|
|
--distribution-id E1A2B3C4D5E6F7 \
|
|
|
|
|
--paths "/index.html" "/css/*" "/js/*"
|
|
|
|
|
|
|
|
|
|
# Invalidate everything (costs apply per path)
|
|
|
|
|
aws cloudfront create-invalidation \
|
|
|
|
|
--distribution-id E1A2B3C4D5E6F7 \
|
|
|
|
|
--paths "/*"
|
|
|
|
|
|
|
|
|
|
# Check invalidation status
|
|
|
|
|
aws cloudfront get-invalidation \
|
|
|
|
|
--distribution-id E1A2B3C4D5E6F7 \
|
|
|
|
|
--id I1A2B3C4D5E6F7
|
|
|
|
|
|
|
|
|
|
# List recent invalidations
|
|
|
|
|
aws cloudfront list-invalidations --distribution-id E1A2B3C4D5E6F7
|
2026-01-27 17:35:45 -05:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
### CloudFront Functions (Lightweight Edge Logic)
|
2026-01-27 17:35:45 -05:00
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
```javascript
|
|
|
|
|
// URL rewrite function — add index.html to directory requests
|
|
|
|
|
function handler(event) {
|
|
|
|
|
var request = event.request;
|
|
|
|
|
var uri = request.uri;
|
|
|
|
|
|
|
|
|
|
if (uri.endsWith('/')) {
|
|
|
|
|
request.uri += 'index.html';
|
|
|
|
|
} else if (!uri.includes('.')) {
|
|
|
|
|
request.uri += '/index.html';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return request;
|
2026-01-27 17:35:45 -05:00
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
### CloudFront with Terraform
|
2026-01-27 17:35:45 -05:00
|
|
|
|
2026-03-24 18:02:50 -04:00
|
|
|
```hcl
|
|
|
|
|
# cloudfront.tf
|
|
|
|
|
resource "aws_cloudfront_distribution" "site" {
|
|
|
|
|
enabled = true
|
|
|
|
|
is_ipv6_enabled = true
|
|
|
|
|
default_root_object = "index.html"
|
|
|
|
|
aliases = ["www.example.com"]
|
|
|
|
|
price_class = "PriceClass_100"
|
|
|
|
|
|
|
|
|
|
origin {
|
|
|
|
|
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
|
|
|
|
|
origin_id = "s3-origin"
|
|
|
|
|
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default_cache_behavior {
|
|
|
|
|
allowed_methods = ["GET", "HEAD"]
|
|
|
|
|
cached_methods = ["GET", "HEAD"]
|
|
|
|
|
target_origin_id = "s3-origin"
|
|
|
|
|
|
|
|
|
|
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized
|
|
|
|
|
origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin
|
|
|
|
|
|
|
|
|
|
viewer_protocol_policy = "redirect-to-https"
|
|
|
|
|
compress = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# SPA fallback
|
|
|
|
|
custom_error_response {
|
|
|
|
|
error_code = 404
|
|
|
|
|
response_code = 200
|
|
|
|
|
response_page_path = "/index.html"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# API pass-through (no caching)
|
|
|
|
|
ordered_cache_behavior {
|
|
|
|
|
path_pattern = "/api/*"
|
|
|
|
|
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
|
|
|
|
|
cached_methods = ["GET", "HEAD"]
|
|
|
|
|
target_origin_id = "api-origin"
|
|
|
|
|
|
|
|
|
|
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled
|
|
|
|
|
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # AllViewerExceptHostHeader
|
|
|
|
|
|
|
|
|
|
viewer_protocol_policy = "https-only"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
viewer_certificate {
|
|
|
|
|
acm_certificate_arn = aws_acm_certificate.cert.arn
|
|
|
|
|
ssl_support_method = "sni-only"
|
|
|
|
|
minimum_protocol_version = "TLSv1.2_2021"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restrictions {
|
|
|
|
|
geo_restriction {
|
|
|
|
|
restriction_type = "none"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "aws_cloudfront_origin_access_control" "oac" {
|
|
|
|
|
name = "s3-oac"
|
|
|
|
|
origin_access_control_origin_type = "s3"
|
|
|
|
|
signing_behavior = "always"
|
|
|
|
|
signing_protocol = "sigv4"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Cloudflare CDN
|
|
|
|
|
|
|
|
|
|
### Zone Setup
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Add a zone
|
|
|
|
|
curl -X POST "https://api.cloudflare.com/client/v4/zones" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"name":"example.com","jump_start":true}'
|
|
|
|
|
|
|
|
|
|
# Get zone ID
|
|
|
|
|
ZONE_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=example.com" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result[0].id')
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Cache Rules (Replacing Page Rules)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Create a cache rule for static assets
|
|
|
|
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_cache_settings/entrypoint" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{
|
|
|
|
|
"rules": [
|
|
|
|
|
{
|
|
|
|
|
"expression": "(http.request.uri.path.extension in {\"css\" \"js\" \"png\" \"jpg\" \"woff2\" \"svg\"})",
|
|
|
|
|
"action": "set_cache_settings",
|
|
|
|
|
"action_parameters": {
|
|
|
|
|
"cache": true,
|
|
|
|
|
"browser_ttl": { "mode": "override_origin", "default": 2592000 },
|
|
|
|
|
"edge_ttl": { "mode": "override_origin", "default": 86400 }
|
|
|
|
|
},
|
|
|
|
|
"description": "Cache static assets aggressively"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Purge Cache
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Purge everything
|
|
|
|
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"purge_everything":true}'
|
|
|
|
|
|
|
|
|
|
# Purge specific URLs
|
|
|
|
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"files":["https://example.com/style.css","https://example.com/app.js"]}'
|
|
|
|
|
|
|
|
|
|
# Purge by prefix
|
|
|
|
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
|
|
|
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"prefixes":["https://example.com/images/"]}'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Cache Headers on Origin
|
|
|
|
|
|
|
|
|
|
### nginx Cache Headers
|
|
|
|
|
|
|
|
|
|
```nginx
|
|
|
|
|
# Immutable hashed assets (fingerprinted filenames)
|
|
|
|
|
location ~* \.(js|css)$ {
|
|
|
|
|
if ($uri ~* "\.[a-f0-9]{8,}\.(js|css)$") {
|
|
|
|
|
expires 1y;
|
|
|
|
|
add_header Cache-Control "public, immutable";
|
|
|
|
|
}
|
|
|
|
|
expires 7d;
|
|
|
|
|
add_header Cache-Control "public, must-revalidate";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Images and fonts
|
|
|
|
|
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff2|ttf)$ {
|
|
|
|
|
expires 30d;
|
|
|
|
|
add_header Cache-Control "public, immutable";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# HTML — always revalidate
|
|
|
|
|
location ~* \.html$ {
|
|
|
|
|
expires -1;
|
|
|
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# API responses — no caching
|
|
|
|
|
location /api/ {
|
|
|
|
|
add_header Cache-Control "no-store, no-cache";
|
|
|
|
|
add_header Vary "Authorization, Accept";
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Cache-Control Cheat Sheet
|
|
|
|
|
|
|
|
|
|
| Header | Meaning |
|
|
|
|
|
|--------|---------|
|
|
|
|
|
| `public, max-age=31536000, immutable` | Cache for 1 year, never revalidate (hashed assets) |
|
|
|
|
|
| `public, max-age=86400, must-revalidate` | Cache 1 day, check freshness after |
|
|
|
|
|
| `private, max-age=600` | Browser cache only, 10 min (user-specific content) |
|
|
|
|
|
| `no-cache` | Always revalidate with origin before serving |
|
|
|
|
|
| `no-store` | Never cache (sensitive data) |
|
|
|
|
|
| `s-maxage=3600` | CDN caches for 1 hour, overrides `max-age` for shared caches |
|
|
|
|
|
|
|
|
|
|
## Cache Warming
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Warm cache for critical pages after deployment
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
URLS=(
|
|
|
|
|
"https://www.example.com/"
|
|
|
|
|
"https://www.example.com/products"
|
|
|
|
|
"https://www.example.com/about"
|
|
|
|
|
"https://www.example.com/css/main.abc123.css"
|
|
|
|
|
"https://www.example.com/js/app.def456.js"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for url in "${URLS[@]}"; do
|
|
|
|
|
curl -s -o /dev/null -w "%{http_code} %{time_total}s %{url_effective}\n" "$url"
|
|
|
|
|
done
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Monitoring Cache Performance
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Check cache status from response headers
|
|
|
|
|
curl -sI https://www.example.com/style.css | grep -i -E "cf-cache|x-cache|age|cache-control"
|
|
|
|
|
|
|
|
|
|
# Expected headers:
|
|
|
|
|
# cf-cache-status: HIT (Cloudflare)
|
|
|
|
|
# x-cache: Hit from cloudfront (CloudFront)
|
|
|
|
|
# age: 3600 (seconds since cached)
|
|
|
|
|
|
|
|
|
|
# CloudFront cache hit ratio
|
|
|
|
|
aws cloudwatch get-metric-statistics \
|
|
|
|
|
--namespace AWS/CloudFront \
|
|
|
|
|
--metric-name CacheHitRate \
|
|
|
|
|
--dimensions Name=DistributionId,Value=E1A2B3C4D5E6F7 \
|
|
|
|
|
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
|
|
|
|
|
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
|
|
|
|
|
--period 300 \
|
|
|
|
|
--statistics Average
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Troubleshooting
|
|
|
|
|
|
|
|
|
|
| Symptom | Cause | Fix |
|
|
|
|
|
|---------|-------|-----|
|
|
|
|
|
| `cf-cache-status: DYNAMIC` | No cache rule matches or Cache-Control prevents it | Set `s-maxage` or create a cache rule for the path |
|
|
|
|
|
| Cache hit ratio below 50% | Low TTLs or high URL cardinality (query strings) | Increase TTL; strip unnecessary query strings in cache key |
|
|
|
|
|
| Stale content after deploy | Old objects still cached at edge | Invalidate; use content-hashed filenames to avoid this entirely |
|
|
|
|
|
| CORS errors through CDN | CDN strips or caches wrong `Vary` header | Add `Vary: Origin` and configure origin request policy to forward `Origin` |
|
|
|
|
|
| 502 errors from CDN | Origin down or timeout | Check origin health; increase CDN origin timeout settings |
|
|
|
|
|
| Mixed content warnings | CDN serves HTTPS but origin links use HTTP | Set `viewer-protocol-policy: redirect-to-https`; fix origin URLs |
|
|
|
|
|
| High invalidation costs | Purging `/*` on every deploy | Use fingerprinted filenames; only invalidate `index.html` |
|
|
|
|
|
|
|
|
|
|
## Related Skills
|
|
|
|
|
|
|
|
|
|
- [dns-management](../dns-management/) - DNS records for CDN CNAME setup
|
|
|
|
|
- [cloudflare-pages](../../cloudflare/cloudflare-pages/) - Cloudflare's built-in CDN for Pages projects
|
|
|
|
|
- [reverse-proxy](../reverse-proxy/) - Origin server configuration behind CDN
|
|
|
|
|
- [load-balancing](../load-balancing/) - Multi-origin CDN backends
|