AWS CloudFront: CDN and Edge Computing
Accelerating content delivery with AWS CloudFront. Edge locations, caching strategies, Lambda@Edge, CloudFront Functions, and security best practices.
AWS CloudFront: CDN and Edge Computing
AWS CloudFront is a global content delivery network that accelerates delivery of websites, APIs, and other web content. With edge locations worldwide and integration with Lambda@Edge and CloudFront Functions, it enables both content caching and edge computing capabilities.
CloudFront Fundamentals
Architecture Overview
CloudFront Architecture:
Users → Edge Location → Regional Edge Cache → Origin
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Edge │────▶│ Regional │────▶│ Origin │
│ Location │ │ Edge │ │ (S3/ALB) │
│ (400+) │ │ Cache │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
CloudFront Functions Lambda@Edge
(viewer request/ (origin/viewer
response) request/response)Key Concepts
| Concept | Description |
|---|---|
| Distribution | Configuration for content delivery |
| Origin | Source of content (S3, ALB, custom) |
| Behaviour | Rules for how requests are handled |
| Cache Policy | Rules for caching content |
| TTL | Time to live for cached content |
| Invalidation | Forcing cache refresh |
Distribution Configuration
Static Website with S3 Origin
# cloudfront.tf
resource "aws_cloudfront_distribution" "website" {
enabled = true
is_ipv6_enabled = true
comment = "${var.project} website distribution"
default_root_object = "index.html"
aliases = [var.domain_name, "www.${var.domain_name}"]
price_class = "PriceClass_100" # North America and Europe
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.website.id}"
origin_access_control_id = aws_cloudfront_origin_access_control.website.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.website.id}"
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.static.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.static.id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.url_rewrite.arn
}
}
# Custom error pages for SPA
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
error_caching_min_ttl = 300
}
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
error_caching_min_ttl = 300
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.website.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
tags = var.tags
}
resource "aws_cloudfront_origin_access_control" "website" {
name = "${var.project}-oac"
description = "OAC for ${var.project} website"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# S3 bucket policy for CloudFront access
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowCloudFrontServicePrincipal"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.website.arn
}
}
}]
})
}API with ALB Origin
# api-distribution.tf
resource "aws_cloudfront_distribution" "api" {
enabled = true
is_ipv6_enabled = true
comment = "${var.project} API distribution"
aliases = ["api.${var.domain_name}"]
origin {
domain_name = aws_lb.api.dns_name
origin_id = "ALB-${aws_lb.api.name}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
custom_header {
name = "X-CloudFront-Secret"
value = var.cloudfront_secret
}
}
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
target_origin_id = "ALB-${aws_lb.api.name}"
viewer_protocol_policy = "https-only"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.api.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.api.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.security.id
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.api.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
web_acl_id = aws_wafv2_web_acl.api.arn
tags = var.tags
}Cache Policies
Custom Cache Policies
# cache-policies.tf
resource "aws_cloudfront_cache_policy" "static" {
name = "${var.project}-static-cache"
comment = "Cache policy for static assets"
default_ttl = 86400 # 1 day
max_ttl = 31536000 # 1 year
min_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
resource "aws_cloudfront_cache_policy" "api" {
name = "${var.project}-api-cache"
comment = "Cache policy for API responses"
default_ttl = 0
max_ttl = 3600
min_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Authorization", "Accept"]
}
}
query_strings_config {
query_string_behavior = "all"
}
}
}
resource "aws_cloudfront_origin_request_policy" "api" {
name = "${var.project}-api-origin"
comment = "Origin request policy for API"
cookies_config {
cookie_behavior = "all"
}
headers_config {
header_behavior = "allViewer"
}
query_strings_config {
query_string_behavior = "all"
}
}CloudFront Functions
URL Rewriting
// url-rewrite.js (CloudFront Function)
function handler(event) {
var request = event.request;
var uri = request.uri;
// Add index.html to directory requests
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// Add .html extension for clean URLs
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}Security Headers
// security-headers.js (CloudFront Function)
function handler(event) {
var response = event.response;
var headers = response.headers;
// Security headers
headers['strict-transport-security'] = {
value: 'max-age=63072000; includeSubdomains; preload'
};
headers['x-content-type-options'] = {
value: 'nosniff'
};
headers['x-frame-options'] = {
value: 'DENY'
};
headers['x-xss-protection'] = {
value: '1; mode=block'
};
headers['referrer-policy'] = {
value: 'strict-origin-when-cross-origin'
};
headers['content-security-policy'] = {
value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com"
};
return response;
}Terraform Configuration for Functions
# functions.tf
resource "aws_cloudfront_function" "url_rewrite" {
name = "${var.project}-url-rewrite"
runtime = "cloudfront-js-2.0"
comment = "URL rewriting for SPA"
publish = true
code = file("${path.module}/functions/url-rewrite.js")
}
resource "aws_cloudfront_function" "security_headers" {
name = "${var.project}-security-headers"
runtime = "cloudfront-js-2.0"
comment = "Add security headers"
publish = true
code = file("${path.module}/functions/security-headers.js")
}Lambda@Edge
A/B Testing at the Edge
// ab-test.ts (Lambda@Edge)
import { CloudFrontRequestEvent, CloudFrontRequestResult } from 'aws-lambda';
const EXPERIMENT_COOKIE = 'experiment_variant';
const VARIANTS = ['A', 'B'];
const VARIANT_WEIGHTS = [50, 50]; // 50% each
export const handler = async (
event: CloudFrontRequestEvent
): Promise<CloudFrontRequestResult> => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Check for existing variant cookie
let variant = getVariantFromCookie(headers);
if (!variant) {
// Assign new variant
variant = assignVariant();
// Add cookie to request for origin
const cookieValue = `${EXPERIMENT_COOKIE}=${variant}; Path=/; Secure; SameSite=Lax`;
headers.cookie = headers.cookie || [];
headers.cookie.push({ key: 'Cookie', value: cookieValue });
}
// Route to variant-specific origin or path
if (variant === 'B') {
request.origin = {
s3: {
domainName: 'variant-b-bucket.s3.amazonaws.com',
region: 'eu-west-1',
authMethod: 'origin-access-identity',
path: '',
customHeaders: {}
}
};
request.headers.host = [{ key: 'Host', value: 'variant-b-bucket.s3.amazonaws.com' }];
}
return request;
};
const getVariantFromCookie = (
headers: Record<string, Array<{ key?: string; value: string }>>
): string | null => {
const cookies = headers.cookie?.[0]?.value || '';
const match = cookies.match(new RegExp(`${EXPERIMENT_COOKIE}=([AB])`));
return match ? match[1] : null;
};
const assignVariant = (): string => {
const random = Math.random() * 100;
let cumulative = 0;
for (let i = 0; i < VARIANTS.length; i++) {
cumulative += VARIANT_WEIGHTS[i];
if (random <= cumulative) {
return VARIANTS[i];
}
}
return VARIANTS[0];
};Image Optimisation
// image-resize.ts (Lambda@Edge - Origin Response)
import { CloudFrontResponseEvent, CloudFrontResponseResult } from 'aws-lambda';
import sharp from 'sharp';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'eu-west-1' });
export const handler = async (
event: CloudFrontResponseEvent
): Promise<CloudFrontResponseResult> => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
// Only process images
if (!request.uri.match(/\.(jpg|jpeg|png|webp)$/i)) {
return response;
}
// Check for resize parameters
const params = new URLSearchParams(request.querystring);
const width = parseInt(params.get('w') || '0');
const quality = parseInt(params.get('q') || '80');
if (!width || response.status !== '200') {
return response;
}
try {
// Get original image from S3
const bucket = request.origin?.s3?.domainName.split('.')[0];
const key = request.uri.substring(1);
const s3Response = await s3.send(new GetObjectCommand({
Bucket: bucket,
Key: key
}));
const originalImage = await s3Response.Body?.transformToByteArray();
if (!originalImage) {
return response;
}
// Resize image
const resizedImage = await sharp(originalImage)
.resize(width, null, { withoutEnlargement: true })
.webp({ quality })
.toBuffer();
// Return resized image
response.status = '200';
response.statusDescription = 'OK';
response.body = resizedImage.toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/webp' }];
response.headers['cache-control'] = [{ key: 'Cache-Control', value: 'public, max-age=31536000' }];
return response;
} catch (error) {
console.error('Image resize error:', error);
return response;
}
};Security Best Practices
WAF Integration
# waf.tf
resource "aws_wafv2_web_acl" "cloudfront" {
name = "${var.project}-cloudfront-waf"
description = "WAF for CloudFront distribution"
scope = "CLOUDFRONT"
provider = aws.us-east-1 # WAF for CloudFront must be in us-east-1
default_action {
allow {}
}
# AWS Managed Rules - Common Rule Set
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesCommonRuleSetMetric"
sampled_requests_enabled = true
}
}
# Rate limiting
rule {
name = "RateLimitRule"
priority = 2
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRuleMetric"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CloudFrontWAFMetric"
sampled_requests_enabled = true
}
}Monitoring
CloudWatch Dashboard
# monitoring.tf
resource "aws_cloudwatch_dashboard" "cloudfront" {
dashboard_name = "${var.project}-cloudfront"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/CloudFront", "Requests", "DistributionId", aws_cloudfront_distribution.website.id, "Region", "Global"],
[".", "BytesDownloaded", ".", ".", ".", "."]
]
title = "Requests and Bytes"
region = "us-east-1"
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/CloudFront", "CacheHitRate", "DistributionId", aws_cloudfront_distribution.website.id, "Region", "Global"]
]
title = "Cache Hit Rate"
region = "us-east-1"
}
}
]
})
}Key Takeaways
-
Use Origin Access Control: Secure S3 origins with OAC instead of OAI
-
Cache strategically: Different content types need different cache policies
-
Compress content: Enable Brotli and Gzip compression for faster delivery
-
Security headers: Use CloudFront Functions or response headers policies
-
WAF integration: Protect against common attacks at the edge
-
Invalidate wisely: Use versioned file names instead of frequent invalidations
-
Monitor cache hit rate: Low hit rates mean inefficient caching configuration
-
Edge computing: Use CloudFront Functions for simple logic, Lambda@Edge for complex
CloudFront brings content closer to users while providing security and compute capabilities at the edge. Proper configuration maximises performance and minimises origin load.