cdn-setup

Installation
SKILL.md

CDN Setup

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).

AWS CloudFront

Create a Distribution via CLI

# Create an S3 origin distribution with OAC (Origin Access Control)
aws cloudfront create-distribution --distribution-config '{
  "CallerReference": "my-site-'$(date +%s)'",
  "Comment": "Production site CDN",
  "Enabled": true,
  "Origins": {
    "Quantity": 1,
    "Items": [{
      "Id": "s3-origin",
      "DomainName": "my-bucket.s3.us-east-1.amazonaws.com",
      "OriginPath": "",
      "S3OriginConfig": {
        "OriginAccessIdentity": ""
      },
      "OriginAccessControlId": "E2QWRUHAPOMQZL"
    }]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "s3-origin",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET", "HEAD"]
    },
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "Compress": true
  },
  "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
    }]
  }
}'

Cache Invalidation

# 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

CloudFront Functions (Lightweight Edge Logic)

// 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;
}

CloudFront with Terraform

# 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

# 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)

# 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

# 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

# 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

# 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

# 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

Weekly Installs
31
GitHub Stars
18
First Seen
5 days ago