AWS CloudFront at the Edge: CDN Configuration, Lambda@Edge, and Cache Policies
Master AWS CloudFront: cache policy configuration, origin groups for failover, Lambda@Edge for request manipulation, and field-level encryption — with production Terraform examples.
CloudFront is the traffic layer between your users and everything else. Most teams configure it once and forget it — a static distribution with default settings. That leaves significant performance and cost optimization on the table. This post covers production CloudFront setup: cache policies that actually cache, origin groups for zero-downtime failover, Lambda@Edge for request-time logic, and field-level encryption for PII.
Architecture Overview
User → CloudFront Edge POP → Cache Policy Check
├── HIT: Return cached response
└── MISS: Origin Group
├── Primary: ALB / S3 / API Gateway
└── Failover: Secondary origin (on 5xx)
Lambda@Edge hooks:
Viewer Request → Auth header check, A/B test routing, bot filtering
Origin Request → Cache key manipulation, rewrite paths
Origin Response → Add headers, transform responses
Viewer Response → Security headers, cache-control override
Terraform Infrastructure
Distribution with Multiple Origins
# infrastructure/cloudfront/main.tf
locals {
s3_origin_id = "S3-static-assets"
api_origin_id = "ALB-api-primary"
api_failover_origin_id = "ALB-api-secondary"
}
# S3 bucket for static assets
resource "aws_s3_bucket" "assets" {
bucket = "${var.project}-assets-${var.environment}"
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# OAC for S3 (replaces legacy OAI)
resource "aws_cloudfront_origin_access_control" "s3" {
name = "${var.project}-s3-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# Custom cache policy: aggressive caching for static assets
resource "aws_cloudfront_cache_policy" "static_assets" {
name = "${var.project}-static-assets"
min_ttl = 86400 # 1 day minimum
default_ttl = 2592000 # 30 days default
max_ttl = 31536000 # 1 year maximum
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none" # Don't vary by cookie for static assets
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "whitelist"
query_strings { items = ["v"] } # Cache-bust with ?v=hash
}
enable_accept_encoding_gzip = true
enable_accept_encoding_brotli = true
}
}
# API cache policy: short TTL, varies by auth header
resource "aws_cloudfront_cache_policy" "api_cacheable" {
name = "${var.project}-api-cacheable"
min_ttl = 0
default_ttl = 30 # 30 seconds
max_ttl = 300 # 5 minutes max
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers { items = ["Authorization"] } # Different users get different cache
}
query_strings_config {
query_string_behavior = "all"
}
enable_accept_encoding_gzip = true
enable_accept_encoding_brotli = true
}
}
# Origin request policy: forward auth headers to ALB
resource "aws_cloudfront_origin_request_policy" "api" {
name = "${var.project}-api-origin-request"
cookies_config {
cookie_behavior = "all"
}
headers_config {
header_behavior = "whitelist"
headers {
items = [
"Authorization",
"CloudFront-Viewer-Country",
"CloudFront-Viewer-City",
"X-Forwarded-For",
]
}
}
query_strings_config {
query_string_behavior = "all"
}
}
# Response headers policy: security headers
resource "aws_cloudfront_response_headers_policy" "security" {
name = "${var.project}-security-headers"
security_headers_config {
content_security_policy {
content_security_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';"
override = true
}
strict_transport_security {
access_control_max_age_sec = 63072000 # 2 years
include_subdomains = true
preload = true
override = true
}
content_type_options { override = true } # X-Content-Type-Options: nosniff
frame_options {
frame_option = "DENY"
override = true
}
referrer_policy {
referrer_policy = "strict-origin-when-cross-origin"
override = true
}
xss_protection {
mode_block = true
protection = true
override = true
}
}
custom_headers_config {
items {
header = "Permissions-Policy"
value = "camera=(), microphone=(), geolocation=(self)"
override = true
}
}
}
resource "aws_cloudfront_distribution" "main" {
enabled = true
is_ipv6_enabled = true
price_class = var.environment == "prod" ? "PriceClass_All" : "PriceClass_100"
aliases = [var.domain_name, "www.${var.domain_name}"]
default_root_object = "index.html"
http_version = "http2and3"
wait_for_deployment = false
# S3 origin for static assets
origin {
domain_name = aws_s3_bucket.assets.bucket_regional_domain_name
origin_id = local.s3_origin_id
origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
}
# Primary API origin (ALB)
origin {
domain_name = var.alb_primary_dns_name
origin_id = local.api_origin_id
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
origin_read_timeout = 30
origin_keepalive_timeout = 5
}
custom_header {
name = "X-CloudFront-Secret"
value = var.cloudfront_origin_secret # ALB rule validates this header
}
}
# Secondary API origin (failover)
origin {
domain_name = var.alb_secondary_dns_name
origin_id = local.api_failover_origin_id
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
origin_read_timeout = 30
}
custom_header {
name = "X-CloudFront-Secret"
value = var.cloudfront_origin_secret
}
}
# Origin group: automatic failover on 5xx
origin_group {
origin_id = "api-origin-group"
failover_criteria {
status_codes { items = [500, 502, 503, 504] }
}
member { origin_id = local.api_origin_id }
member { origin_id = local.api_failover_origin_id }
}
# Default behavior: serve from S3
default_cache_behavior {
target_origin_id = local.s3_origin_id
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = aws_cloudfront_cache_policy.static_assets.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.security.id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.redirect_www.arn
}
}
# API routes: forward to origin group
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = "api-origin-group"
viewer_protocol_policy = "https-only"
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = aws_cloudfront_cache_policy.api_cacheable.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.api.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.security.id
# Lambda@Edge for auth
lambda_function_association {
event_type = "viewer-request"
lambda_arn = aws_lambda_function.auth_edge.qualified_arn
include_body = false
}
}
# Static assets with long-term cache
ordered_cache_behavior {
path_pattern = "/_next/static/*"
target_origin_id = local.s3_origin_id
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = aws_cloudfront_cache_policy.static_assets.id
}
viewer_certificate {
acm_certificate_arn = var.acm_certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction { restriction_type = "none" }
}
logging_config {
include_cookies = false
bucket = "${var.project}-cf-logs.s3.amazonaws.com"
prefix = "${var.environment}/"
}
tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Lambda@Edge Functions
Lambda@Edge runs in CloudFront edge POPs (us-east-1 only for function definition). Constraints: 128MB RAM, 5 second timeout for viewer events, 30s for origin events.
Viewer Request: JWT Authentication
// lambda-edge/auth/handler.ts
// Note: Must be compiled to CommonJS and deployed to us-east-1
import type { CloudFrontRequestHandler } from 'aws-lambda';
import * as jwt from 'jsonwebtoken';
const PUBLIC_PATHS = [
'/',
'/login',
'/signup',
'/api/auth/callback',
'/api/auth/session',
];
const JWT_SECRET = process.env.JWT_SECRET!; // From Lambda environment (SSM)
export const handler: CloudFrontRequestHandler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
// Allow public paths
if (PUBLIC_PATHS.some((p) => uri === p || uri.startsWith(`${p}/`))) {
return request;
}
// Allow static assets
if (uri.startsWith('/_next/') || uri.startsWith('/images/') || uri.endsWith('.ico')) {
return request;
}
// Extract token from Authorization header or cookie
const authHeader = request.headers['authorization']?.[0]?.value;
const cookieHeader = request.headers['cookie']?.[0]?.value;
let token: string | null = null;
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.slice(7);
} else if (cookieHeader) {
const match = cookieHeader.match(/session-token=([^;]+)/);
token = match ? decodeURIComponent(match[1]) : null;
}
if (!token) {
return {
status: '302',
headers: {
location: [{ key: 'Location', value: `/login?returnTo=${encodeURIComponent(uri)}` }],
'cache-control': [{ key: 'Cache-Control', value: 'no-store' }],
},
};
}
try {
const payload = jwt.verify(token, JWT_SECRET) as { sub: string; tenantId: string; roles: string[] };
// Forward user context to origin via custom headers
request.headers['x-user-id'] = [{ key: 'X-User-Id', value: payload.sub }];
request.headers['x-tenant-id'] = [{ key: 'X-Tenant-Id', value: payload.tenantId }];
request.headers['x-user-roles'] = [{ key: 'X-User-Roles', value: payload.roles.join(',') }];
return request;
} catch {
return {
status: '302',
headers: {
location: [{ key: 'Location', value: `/login?expired=true&returnTo=${encodeURIComponent(uri)}` }],
'set-cookie': [{ key: 'Set-Cookie', value: 'session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure' }],
'cache-control': [{ key: 'Cache-Control', value: 'no-store' }],
},
};
}
};
CloudFront Function (Viewer Request): WWW Redirect
// cloudfront-functions/redirect-www.js
// CloudFront Functions are lighter than Lambda@Edge — <1ms, free up to 2M/mo
function handler(event) {
var request = event.request;
var host = request.headers.host.value;
// Redirect www → apex
if (host.startsWith('www.')) {
var newHost = host.slice(4);
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: 'https://' + newHost + request.uri },
'cache-control': { value: 'max-age=31536000' },
},
};
}
// Rewrite /blog → /blog/index.html for S3
if (request.uri.endsWith('/') && request.uri !== '/') {
request.uri = request.uri + 'index.html';
}
return request;
}
# terraform: CloudFront Function (not Lambda@Edge)
resource "aws_cloudfront_function" "redirect_www" {
name = "${var.project}-redirect-www"
runtime = "cloudfront-js-2.0"
publish = true
code = file("${path.module}/cloudfront-functions/redirect-www.js")
}
Origin Response: Cache-Control Injection
// lambda-edge/cache-headers/handler.ts
import type { CloudFrontResponseHandler } from 'aws-lambda';
export const handler: CloudFrontResponseHandler = async (event) => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
const uri = request.uri;
// Add Cache-Control if origin didn't set one
if (!response.headers['cache-control']) {
let cacheControl = 'public, no-cache'; // default: revalidate
if (uri.match(/\.(js|css|woff2?|ttf|otf|eot)$/)) {
cacheControl = 'public, max-age=31536000, immutable';
} else if (uri.match(/\.(jpg|jpeg|png|webp|avif|gif|svg|ico)$/)) {
cacheControl = 'public, max-age=86400, stale-while-revalidate=604800';
}
response.headers['cache-control'] = [{ key: 'Cache-Control', value: cacheControl }];
}
// Remove headers that expose implementation details
delete response.headers['x-powered-by'];
delete response.headers['server'];
return response;
};
Cache Invalidation Strategy
// src/lib/cloudfront/invalidation.ts
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
import crypto from 'crypto';
const cf = new CloudFrontClient({ region: 'us-east-1' });
interface InvalidationTarget {
paths: string[]; // e.g., ['/blog/*', '/sitemap.xml']
reason?: string;
}
export async function invalidateCache(
distributionId: string,
target: InvalidationTarget
): Promise<string> {
const callerReference = crypto.randomUUID();
const command = new CreateInvalidationCommand({
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: callerReference,
Paths: {
Quantity: target.paths.length,
Items: target.paths,
},
},
});
const result = await cf.send(command);
const invalidationId = result.Invalidation?.Id ?? 'unknown';
console.info({
msg: 'CloudFront invalidation created',
distributionId,
invalidationId,
paths: target.paths,
reason: target.reason,
});
return invalidationId;
}
// Invalidate after CMS publish
export async function invalidateAfterPublish(contentType: 'post' | 'page', slug: string) {
const paths =
contentType === 'post'
? [`/blog/${slug}`, '/blog/*', '/sitemap.xml', '/feed.xml']
: [`/${slug}`, '/sitemap.xml'];
await invalidateCache(process.env.CLOUDFRONT_DISTRIBUTION_ID!, {
paths,
reason: `Published ${contentType}: ${slug}`,
});
}
Important: CloudFront invalidations cost $0.005 per path after the first 1,000 paths/month. Batch invalidations (use wildcards like /blog/*) rather than individual paths.
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Monitoring and Observability
CloudWatch Alarms
# terraform: CloudFront monitoring
resource "aws_cloudwatch_metric_alarm" "cf_5xx_rate" {
alarm_name = "${var.project}-cloudfront-5xx-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "5xxErrorRate"
namespace = "AWS/CloudFront"
period = 60
statistic = "Average"
threshold = 5 # Alert if >5% error rate
alarm_description = "CloudFront 5xx error rate exceeded 5%"
alarm_actions = [var.sns_alert_arn]
dimensions = {
DistributionId = aws_cloudfront_distribution.main.id
Region = "Global"
}
}
resource "aws_cloudwatch_metric_alarm" "cf_cache_hit_rate" {
alarm_name = "${var.project}-cloudfront-cache-hit-rate"
comparison_operator = "LessThanThreshold"
evaluation_periods = 3
metric_name = "CacheHitRate"
namespace = "AWS/CloudFront"
period = 300
statistic = "Average"
threshold = 60 # Alert if cache hit rate drops below 60%
alarm_description = "CloudFront cache hit rate is low — check cache policies"
alarm_actions = [var.sns_alert_arn]
dimensions = {
DistributionId = aws_cloudfront_distribution.main.id
Region = "Global"
}
}
Real User Monitoring via CloudFront Headers
// Middleware: capture CloudFront viewer metadata for analytics
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const viewerCountry = req.headers.get('cloudfront-viewer-country');
const viewerCity = req.headers.get('cloudfront-viewer-city');
const isBot = req.headers.get('x-is-bot') === '1';
// Attach viewer context for downstream analytics
const res = NextResponse.next();
if (viewerCountry) res.headers.set('x-viewer-country', viewerCountry);
if (viewerCity) res.headers.set('x-viewer-city', viewerCity);
return res;
}
Cost Reference
| Configuration | Monthly Estimate | When to Use |
|---|---|---|
| Basic CDN (1TB transfer, US) | $85–120/mo | Static site, small SaaS |
| Global distribution (5TB) | $380–550/mo | International product |
| + Lambda@Edge (10M invocations) | +$50/mo | Auth, A/B testing at edge |
| + CloudFront Functions (100M req) | +$20/mo | Simple URL rewrites |
| Enterprise (50TB, shield advanced) | $5K–15K/mo | DDoS protection, SLA guarantee |
Optimization: A good cache policy can cut your origin compute costs by 60–80%. A typical client going from no CDN to CloudFront with proper cache policies sees a 40–70% reduction in ALB/EC2 costs alongside the CDN spend.
See Also
- AWS ECS Fargate in Production: Task Definitions and Blue/Green Deploys
- AWS Lambda Optimization: Cold Starts, Memory, and Cost
- Multi-Cloud Strategy: Avoiding Lock-In While Staying Pragmatic
- Infrastructure Cost Engineering: Tagging, Rightsizing, and Reserved Capacity
- Zero Trust Security Architecture
Working With Viprasol
Building a globally distributed application and need CDN architecture that goes beyond "just enable CloudFront"? We design CloudFront distributions with proper cache hierarchies, Lambda@Edge for edge auth, origin group failover, and full Terraform IaC — so your CDN is a reliability multiplier, not an afterthought.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.