CDN Media Delivery
Guidance for configuring CDN delivery, cache management, and secure media access for headless CMS architectures.
When to Use This Skill
-
Configuring CDN for media delivery
-
Implementing cache invalidation strategies
-
Setting up signed/secure URLs
-
Optimizing edge caching
-
Configuring origin shielding
CDN Architecture
Basic CDN Setup
┌─────────────────────────────────────────────────────────────┐ │ Users │ │ (Global, geographically distributed) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ CDN Edge Network │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Edge │ │ Edge │ │ Edge │ │ Edge │ │ │ │ US-West │ │ US-East │ │ Europe │ │ Asia │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Origin Shield │ │ (Optional intermediate cache layer) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Origin Server │ │ ┌───────────────┐ ┌────────────────┐ ┌───────────────┐ │ │ │ Media API │ │ Blob Storage │ │ Image │ │ │ │ (transform) │ │ (Azure/S3) │ │ Processor │ │ │ └───────────────┘ └────────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────┘
CDN Configuration
Azure CDN (Front Door)
// appsettings.json { "Cdn": { "Provider": "AzureFrontDoor", "Endpoint": "https://media.example.com", "OriginHost": "storage.blob.core.windows.net", "CacheRules": { "Images": { "CacheDuration": "365.00:00:00", "QueryStringCaching": "IgnoreQueryString" }, "Transforms": { "CacheDuration": "30.00:00:00", "QueryStringCaching": "UseQueryString" } } } }
CloudFront Configuration
public class CloudFrontConfiguration { public string DistributionId { get; set; } = string.Empty; public string DomainName { get; set; } = string.Empty; public string OriginId { get; set; } = string.Empty;
public CacheBehavior DefaultCacheBehavior { get; set; } = new()
{
ViewerProtocolPolicy = "redirect-to-https",
CachePolicyId = "658327ea-f89d-4fab-a63d-7e88639e58f6", // CachingOptimized
Compress = true,
AllowedMethods = new[] { "GET", "HEAD", "OPTIONS" },
CachedMethods = new[] { "GET", "HEAD" }
};
public CacheBehavior[] CacheBehaviors { get; set; } =
{
new()
{
PathPattern = "/media/transform/*",
CachePolicyId = "custom-transform-policy",
QueryStringCaching = QueryStringCaching.All
}
};
}
Cloudflare Configuration
public class CloudflareConfiguration { public string ZoneId { get; set; } = string.Empty; public string ApiToken { get; set; } = string.Empty;
public PageRule[] PageRules { get; set; } =
{
new()
{
Targets = new[] { "*example.com/media/*" },
Actions = new PageRuleAction
{
CacheLevel = "cache_everything",
EdgeCacheTtl = 2592000, // 30 days
BrowserCacheTtl = 86400 // 1 day
}
}
};
}
Cache Headers
Setting Cache Headers
public class MediaCacheMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { await next(context);
if (context.Request.Path.StartsWithSegments("/media"))
{
var cacheControl = GetCacheControl(context.Request.Path);
context.Response.Headers["Cache-Control"] = cacheControl;
context.Response.Headers["Vary"] = "Accept, Accept-Encoding";
}
}
private string GetCacheControl(PathString path)
{
// Original media: cache for 1 year (immutable content)
if (path.Value?.Contains("/original/") == true)
{
return "public, max-age=31536000, immutable";
}
// Transformed images: cache for 30 days
if (path.Value?.Contains("/transform/") == true)
{
return "public, max-age=2592000, stale-while-revalidate=86400";
}
// Default: 1 day
return "public, max-age=86400";
}
}
Cache-Control Directives
Directive Purpose Example
public
Allow CDN caching Images, static assets
private
Browser only User-specific content
max-age
Cache duration (seconds) max-age=86400 (1 day)
immutable
Never revalidate Versioned assets
stale-while-revalidate
Serve stale while fetching Background refresh
no-cache
Always revalidate Dynamic content
no-store
Never cache Sensitive data
Cache Invalidation
Invalidation Service
public interface ICdnInvalidationService { Task InvalidatePathAsync(string path); Task InvalidatePathsAsync(IEnumerable<string> paths); Task InvalidatePrefixAsync(string prefix); Task InvalidateAllAsync(); }
// Azure CDN implementation public class AzureCdnInvalidationService : ICdnInvalidationService { private readonly CdnManagementClient _cdnClient;
public async Task InvalidatePathAsync(string path)
{
await _cdnClient.Endpoints.PurgeContentAsync(
_resourceGroup,
_profileName,
_endpointName,
new PurgeParameters(new[] { path }));
}
public async Task InvalidatePrefixAsync(string prefix)
{
await _cdnClient.Endpoints.PurgeContentAsync(
_resourceGroup,
_profileName,
_endpointName,
new PurgeParameters(new[] { $"{prefix}/*" }));
}
}
// CloudFront implementation public class CloudFrontInvalidationService : ICdnInvalidationService { private readonly AmazonCloudFrontClient _client;
public async Task InvalidatePathAsync(string path)
{
var request = new CreateInvalidationRequest
{
DistributionId = _distributionId,
InvalidationBatch = new InvalidationBatch
{
CallerReference = Guid.NewGuid().ToString(),
Paths = new Paths
{
Items = new List<string> { path },
Quantity = 1
}
}
};
await _client.CreateInvalidationAsync(request);
}
}
Event-Based Invalidation
public class MediaUpdatedHandler : INotificationHandler<MediaUpdatedEvent> { private readonly ICdnInvalidationService _cdn;
public async Task Handle(MediaUpdatedEvent notification, CancellationToken ct)
{
// Invalidate original
await _cdn.InvalidatePathAsync($"/media/{notification.MediaId}");
// Invalidate all transformations
await _cdn.InvalidatePrefixAsync($"/media/transform/{notification.MediaId}");
}
}
Signed URLs
Signed URL Generation
public class SignedUrlService { public string GenerateSignedUrl( string path, TimeSpan validity, SignedUrlOptions? options = null) { options ??= new SignedUrlOptions();
var expiry = DateTime.UtcNow.Add(validity);
var expiryTimestamp = new DateTimeOffset(expiry).ToUnixTimeSeconds();
// Build URL with parameters
var urlBuilder = new UriBuilder($"{_cdnBaseUrl}{path}");
var query = HttpUtility.ParseQueryString(urlBuilder.Query);
query["expires"] = expiryTimestamp.ToString();
if (options.AllowedIp != null)
{
query["ip"] = options.AllowedIp;
}
// Generate signature
var signatureData = $"{path}|{expiryTimestamp}|{options.AllowedIp}";
var signature = ComputeSignature(signatureData);
query["signature"] = signature;
urlBuilder.Query = query.ToString();
return urlBuilder.ToString();
}
private string ComputeSignature(string data)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_signingKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(hash)
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('=');
}
}
public class SignedUrlOptions { public string? AllowedIp { get; set; } public string? AllowedCountry { get; set; } public int? MaxDownloads { get; set; } }
Signed URL Validation
public class SignedUrlValidationMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (RequiresSignedUrl(context.Request.Path)) { var query = context.Request.Query;
// Check expiry
if (!long.TryParse(query["expires"], out var expiry) ||
DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiry)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("URL expired");
return;
}
// Validate signature
var expectedSignature = ComputeSignature(
context.Request.Path,
expiry,
query["ip"]);
if (query["signature"] != expectedSignature)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Invalid signature");
return;
}
// Check IP restriction
if (!string.IsNullOrEmpty(query["ip"]))
{
var clientIp = context.Connection.RemoteIpAddress?.ToString();
if (clientIp != query["ip"])
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("IP not allowed");
return;
}
}
}
await next(context);
}
}
Origin Shielding
Shield Configuration
public class OriginShieldConfiguration { public bool Enabled { get; set; } = true; public string ShieldRegion { get; set; } = "us-east-1"; public int ShieldCacheTtl { get; set; } = 3600; // 1 hour public int MaxConnectionsToOrigin { get; set; } = 100; }
Benefits
Feature Without Shield With Shield
Origin requests From each edge From one region
Cache efficiency Per-edge Shared shield cache
Origin load High Reduced 90%+
Latency Variable Predictable
CDN URL Generation
URL Service
public class CdnUrlService { public string GetMediaUrl(MediaItem media, MediaUrlOptions? options = null) { options ??= new MediaUrlOptions();
var path = $"/media/{media.StoragePath}";
// Add transformation query params
if (options.Width.HasValue || options.Height.HasValue)
{
var query = new List<string>();
if (options.Width.HasValue) query.Add($"w={options.Width}");
if (options.Height.HasValue) query.Add($"h={options.Height}");
if (options.Format.HasValue) query.Add($"format={options.Format}");
if (options.Quality.HasValue) query.Add($"q={options.Quality}");
path += "?" + string.Join("&", query);
}
// Generate signed URL if private
if (media.IsPrivate || options.RequireSignature)
{
return _signedUrlService.GenerateSignedUrl(
path,
options.UrlValidity ?? TimeSpan.FromHours(1));
}
return $"{_cdnBaseUrl}{path}";
}
}
public class MediaUrlOptions { public int? Width { get; set; } public int? Height { get; set; } public ImageFormat? Format { get; set; } public int? Quality { get; set; } public bool RequireSignature { get; set; } public TimeSpan? UrlValidity { get; set; } }
Performance Monitoring
CDN Metrics
public class CdnMetrics { public long TotalRequests { get; set; } public long CacheHits { get; set; } public long CacheMisses { get; set; } public double CacheHitRatio => (double)CacheHits / TotalRequests; public long BandwidthBytes { get; set; } public double AverageLatencyMs { get; set; } public Dictionary<string, long> RequestsByRegion { get; set; } = new(); public Dictionary<int, long> StatusCodeCounts { get; set; } = new(); }
Related Skills
-
media-asset-management
-
Media storage and organization
-
image-optimization
-
Image processing before CDN
-
headless-api-design
-
Media API endpoints