Back to Blog

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.

Viprasol Tech Team
November 1, 2026
13 min read

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

ConfigurationMonthly EstimateWhen to Use
Basic CDN (1TB transfer, US)$85–120/moStatic site, small SaaS
Global distribution (5TB)$380–550/moInternational product
+ Lambda@Edge (10M invocations)+$50/moAuth, A/B testing at edge
+ CloudFront Functions (100M req)+$20/moSimple URL rewrites
Enterprise (50TB, shield advanced)$5K–15K/moDDoS 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


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.

Talk to our team → | Explore our cloud solutions →

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

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.