Architecture

Global image delivery with CloudFront and edge-side transformation

Serving 1B transformed images/day on CloudFront without turning the CDN into a Lambda trigger.

01

The edge — CloudFront in front of S3

flowchart LR Client([Client]) -->|GET img w-300| CF[CloudFront] CF -->|miss| S3[(S3 Originals)] S3 --> CF CF -->|cached bytes| Client

Clients request derivatives by query string; CloudFront caches per object and S3 holds the originals. This is the demo that works until the cache key explodes.

02

Cache-key normalization at the edge

flowchart LR Client([Client]) --> CFF[CloudFront Function\nnormalize key 1ms] CFF -->|w h fmt q only| CF[CloudFront] CF -->|miss| S3[(S3 Originals)] S3 --> CF CF -->|cached bytes| Client

A CloudFront Function on viewer-request sorts and clamps the allowlisted params and strips junk, so utm and session noise never mint new cache objects. Runs in ~1 ms on every request.

03

Transform on miss, with coalescing

flowchart TD Client([Client]) --> CFF[CloudFront Function\nnormalize key] CFF --> CF[CloudFront edge cache] CF -->|hit| Client CF -->|miss| Shield[Origin Shield\ncoalesce misses] Shield --> LE[Lambda at edge\ntransform origin-request] LE --> S3[(S3 Originals)] S3 --> LE LE --> Shield --> CF

Origin Shield (one per CRR region) collapses concurrent misses for the same key into one fetch; Lambda at edge on origin-request transforms only on a miss. Shield coalesces same-key herds, not launch shocks of many distinct keys - those are absorbed by pre-compute.

04

Multi-region origins and failover

flowchart TD Client([Client]) --> CFF[CloudFront Function] CFF --> CF[CloudFront edge cache] CF -->|hit| Client CF -->|miss| Shield[Origin Shield per region] Shield --> LE[Lambda at edge transform] subgraph Origins[Origin Group failover 5xx 403 404] S3a[(S3 us-east-1)] S3b[(S3 eu-west-1)] S3c[(S3 ap-northeast-1)] end LE --> S3a S3a -.CRR plus RTC.-> S3b S3a -.CRR plus RTC.-> S3c LE -.failover.-> S3b

S3 originals replicate to three regions via CRR with Replication Time Control (15-min SLA); CloudFront Origin Groups fail over per request on 5xx plus 403 (IAM) and 404 (replication lag), with no DNS-TTL lag. Keys carry a hash shard prefix for S3 request-rate headroom.

05

Pre-compute as the primary path

flowchart TD Upload([Upload]) --> S3a[(S3 Originals)] S3a -->|S3 event| SF[Step Functions\nvalidate moderate render] SF -->|magic bytes dims| MOD[Rekognition moderation] SF --> S3v[(S3 Pre-computed variants)] SF -->|warm prefetch HEAD| CF[CloudFront edge cache] SF -->|batched| INV[CloudFront Invalidation] Client([Client]) --> CFF[CloudFront Function] CFF --> CF CF -->|hit| Client CF -->|miss| Shield[Origin Shield] Shield --> LE[Lambda at edge transform\nfallback only] LE --> S3v LE --> S3a

On upload, Step Functions validates the image (magic bytes, dimension caps, Rekognition moderation), renders the standard variants to S3, and warm-prefetches them through CloudFront before go-live - so most requests are plain GETs with no Lambda. On-the-fly transform is the long-tail fallback only. Step Functions also batches takedown invalidations within quota.

06

Security and tenant isolation layer

flowchart TD Client([Client]) --> WAF[AWS WAF\nrate limit and bot] WAF --> CFF[CloudFront Function\nsigned URL plus prefix assert] KG[Trusted Key Groups\nper tenant] -.validates.-> CFF CFF --> CF[CloudFront edge cache] CF -->|hit| Client CF -->|miss| Shield[Origin Shield] Shield --> LE[Lambda at edge transform] LE --> S3v[(S3 variants SSE-KMS)] LE --> S3a[(S3 Originals SSE-KMS)] IAM[IAM session-tag\nprefix condition] -.scopes per request.-> LE

Per-tenant Trusted Key Groups validate signed URLs in the viewer Function, which also asserts the requested tenant prefix matches the validating key group and rejects path traversal. The transform role assumes a session tagged with the authenticated tenant ID, so its S3 read is prefix-scoped by IAM condition. SSRF removed by construction; SSE-KMS at rest.

07

The complete system

flowchart TD R53[Route 53\nDNS resolution only] -.resolves cdn name.-> Client Upload([Upload]) --> S3a[(S3 Originals)] S3a -->|event| SF[Step Functions\nvalidate moderate render] SF --> S3v[(S3 variants)] SF --> INV[CloudFront Invalidation] Client([Client]) --> CF subgraph POP[CloudFront POP] WAF[AWS WAF rate and bot] --> CFF[CloudFront Function\nsign normalize prefix assert] CFF --> CF[CloudFront edge cache] end KG[Trusted Key Groups] -.validates.-> CFF CF -->|hit| Client CF -->|miss| Shield[Origin Shield coalesce] Shield --> LE[Lambda at edge transform\nfallback only] subgraph Origins[Origin Group failover 5xx 403 404] S3a S3v S3b[(S3 replica RTC)] end LE --> S3v LE --> S3a LE -.failover.-> S3b CF -.real-time logs.-> FH[Firehose to S3 and CloudWatch] CT[(CloudTrail audit Object Lock)] -.data events.-> S3a

Route 53 resolves DNS only - it is not a hop in the data path. The request enters at the CloudFront POP, where WAF and the signing Function run; then key normalization, hit-or-miss with coalescing, multi-region failover, pre-compute on upload, batched invalidation, real-time-log observability, and tamper-evident CloudTrail audit - all AWS-native.