Load Balancing, CDN & DNS
Table of Contents#
- How a Request Actually Reaches Your Application
- Elastic Load Balancing — The Family
- Application Load Balancer (ALB) — Layer 7
- Target Groups and Health Checks
- ALB Listener Rules — Content-Based Routing
- Network Load Balancer (NLB) — Layer 4
- ALB vs NLB vs Gateway Load Balancer
- Cross-Zone Load Balancing
- Connection Draining — Deregistration Delay
- Sticky Sessions
- CloudFront — AWS's CDN
- CloudFront Caching Behavior
- CloudFront Origins — S3 and Custom Origins
- CloudFront Origin Access Control — Locking Down S3 Origins
- Route 53 — DNS as a Service
- Route 53 Hosted Zones — Public and Private
- Route 53 Routing Policies
- Route 53 Health Checks and DNS Failover
- The Full Request Path, End to End
- TLS/SSL Termination and ACM
- A Full Worked Example: A Global, Multi-Region Web Application
- Compute at the Edge: CloudFront Functions and Lambda@Edge
- S3 Static Website Hosting Behind CloudFront
- Load Balancing and CDN Best Practices — The Consolidated Checklist
- Part 8 CLI Cheat Sheet
- Common Mistakes
- Worked Practice Problems
- Summary and What's Next
How a Request Actually Reaches Your Application#
Every other part of this series has covered compute (Parts 3, 7), storage (Part 5), and networking primitives (Part 4) — this part covers the layer that actually connects a real user, anywhere in the world, to those resources. This is the direct, concrete AWS implementation of the Layer 4 vs Layer 7 load balancing and DNS-based traffic routing concepts already covered generically in the Reliability & Architecture Patterns series (Part 1) and the Linux & Networking series (Part 2).
Diagram
Elastic Load Balancing — The Family#
AWS's Elastic Load Balancing (ELB) service is actually a family of three distinct load balancer types, each suited to a different layer of traffic.
| Type | OSI Layer | Best fit |
|---|---|---|
| Application Load Balancer (ALB) | 7 (HTTP/HTTPS) | Web applications, microservices, content-based routing |
| Network Load Balancer (NLB) | 4 (TCP/UDP) | Extreme performance/low latency, static IP requirements, non-HTTP protocols |
| Gateway Load Balancer (GWLB) | 3 (IP) | Transparently inserting third-party network/security appliances (firewalls, IDS/IPS) into a traffic path |
Application Load Balancer (ALB) — Layer 7#
The ALB understands HTTP/HTTPS at the application layer — meaning it can inspect URL paths, headers, and hostnames to make routing decisions, directly the concrete implementation of "Layer 7 load balancing" already introduced in the Reliability & Architecture Patterns series.
aws elbv2 create-load-balancer \ --name app-alb \ --subnets subnet-public-1a subnet-public-1b \ --security-groups sg-alb123 \ --scheme internet-facing --type application
Diagram
Target Groups and Health Checks#
A Target Group is the set of destinations (EC2 instances, IP addresses, Lambda functions, or ECS tasks, Part 7) an ALB/NLB routes traffic to, along with the health-check configuration determining which targets are actually eligible to receive traffic.
aws elbv2 create-target-group \ --name app-tg --protocol HTTP --port 8080 \ --vpc-id vpc-0123456789abcdef0 \ --health-check-path /healthz \ --health-check-interval-seconds 15 \ --healthy-threshold-count 2 --unhealthy-threshold-count 3 aws elbv2 register-targets \ --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/app-tg \ --targets Id=i-0123456789abcdef0
Why this is directly the same health check mechanism already covered for Auto Scaling Groups in Part 3, worth stating explicitly: this is the SAME target group ARN an ASG's ELB health check type (Part 3) references — a target failing this health check is both removed from load-balanced rotation immediately AND, if the ASG is configured with HealthCheckType: ELB, eventually replaced by the Auto Scaling Group, closing the full loop between traffic routing and self-healing capacity already introduced in Part 3.
ALB Listener Rules — Content-Based Routing#
aws elbv2 create-listener \ --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/app-alb/abc123 \ --protocol HTTPS --port 443 \ --certificates CertificateArn=arn:aws:acm:...:certificate/xyz \ --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/app-tg aws elbv2 create-rule \ --listener-arn arn:aws:elasticloadbalancing:...:listener/app/app-alb/abc123/def456 \ --priority 10 \ --conditions Field=path-pattern,Values='/api/*' \ --actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api-tg
Why content-based routing at the ALB layer is worth stating as a genuinely strong architectural building block, directly connecting to the microservices/canary discussion in the Automation series: it lets ONE load balancer, with ONE public endpoint, route to MULTIPLE distinct backend services based on path or hostname — and the same weighted-forwarding capability (splitting traffic by percentage across two target groups) is exactly how a Canary Deployment (Automation series, Part 1) can be implemented natively at the ALB layer, without any application-level routing logic.
Network Load Balancer (NLB) — Layer 4#
The NLB operates purely at the TCP/UDP level, with no awareness of HTTP content at all — trading ALB's content-based routing intelligence for extreme throughput, ultra-low latency, and a genuinely important, distinct feature: static IP addresses.
aws elbv2 create-load-balancer \ --name app-nlb --subnets subnet-public-1a subnet-public-1b \ --scheme internet-facing --type network
Why static IPs matter specifically, worth stating precisely, a genuinely common real requirement an ALB cannot satisfy: an ALB's underlying IP addresses can change over time (only its DNS name is stable) — some enterprise clients, firewalls, or partner integrations require ALLOWLISTING a fixed, known IP address, which only the NLB (via an Elastic IP per AZ) can provide. NLB is also the standard choice underneath a PrivateLink Interface Endpoint (Part 4) and for any non-HTTP TCP/UDP protocol (a custom game server, an IoT protocol, a database proxy) an ALB structurally cannot route.
ALB vs NLB vs Gateway Load Balancer#
| ALB | NLB | Gateway Load Balancer | |
|---|---|---|---|
| Layer | 7 (HTTP/HTTPS) | 4 (TCP/UDP) | 3 (IP) |
| Content-based routing | Yes (path/host/header) | No | No |
| Static IP | No (DNS name only) | Yes (per AZ) | N/A |
| Throughput/latency | Very good | Best-in-class | N/A |
| Typical use | Web apps, microservices, canary/blue-green routing | Extreme performance, non-HTTP protocols, PrivateLink | Inserting third-party firewalls/IDS transparently into a traffic path |
Cross-Zone Load Balancing#
Worth a precise, dedicated explanation, since it directly affects real traffic distribution fairness across AZs.
Diagram
Without cross-zone load balancing, each load balancer NODE (one per AZ) only distributes traffic to targets WITHIN its own AZ — meaning an uneven target distribution across AZs (2 targets in AZ A, 8 in AZ B) results in the 2 targets in AZ A receiving a disproportionately HIGHER share of traffic per-target, since the AZ A load balancer node has nowhere else to send its share.
aws elbv2 modify-load-balancer-attributes \ --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/app-alb/abc123 \ --attributes Key=load_balancing.cross_zone.enabled,Value=true
Worth knowing precisely: cross-zone load balancing is enabled by DEFAULT (and cannot be disabled) on the ALB — this uneven-distribution risk is specifically an NLB consideration, where it defaults to DISABLED and must be explicitly enabled if target counts aren't perfectly even across AZs.
Connection Draining — Deregistration Delay#
Directly the load-balancer-layer counterpart to the ASG lifecycle hooks already covered in Part 3 — worth understanding as a distinct, complementary mechanism.
aws elbv2 modify-target-group-attributes \ --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/app-tg \ --attributes Key=deregistration_delay.timeout_seconds,Value=30
Why this needs to work TOGETHER with the ASG lifecycle hook from Part 3, worth stating precisely: the deregistration delay controls how long the LOAD BALANCER waits before considering a deregistering target fully drained and stops sending it NEW requests; the ASG lifecycle hook controls how long the INSTANCE itself waits before actually terminating — setting the lifecycle hook's timeout shorter than the target group's deregistration delay would terminate the instance while the load balancer still believes it might be draining, defeating the whole purpose of graceful shutdown.
Sticky Sessions#
For stateful applications needing a user's requests to consistently land on the SAME backend target (worth flagging as generally an anti-pattern for horizontally-scaled, cloud-native design, but a real, common legacy requirement).
aws elbv2 modify-target-group-attributes \ --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/app-tg \ --attributes Key=stickiness.enabled,Value=true Key=stickiness.type,Value=lb_cookie Key=stickiness.lb_cookie.duration_seconds,Value=3600
Why sticky sessions are worth flagging as tension against good horizontal-scaling design, directly connecting to the stateless-vs-stateful discussion in the Capacity Planning series (Part 1): a target that's accumulated a large share of "sticky" users becomes genuinely harder to safely scale in or replace, since doing so disrupts every user stuck to it — the stronger, more cloud-native pattern is externalizing session state entirely (e.g. into ElastiCache, Part 6), removing the need for stickiness altogether.
CloudFront — AWS's CDN#
CloudFront caches content at AWS's globally distributed edge locations (Part 1), serving repeat requests from a location physically close to the user instead of round-tripping to the origin every time.
Diagram
aws cloudfront create-distribution \ --distribution-config '{ "CallerReference": "app-distribution-2026", "Origins": {"Quantity": 1, "Items": [{"Id": "app-alb-origin", "DomainName": "app-alb-123456.us-east-1.elb.amazonaws.com", "CustomOriginConfig": {"HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "https-only"}}]}, "DefaultCacheBehavior": {"TargetOriginId": "app-alb-origin", "ViewerProtocolPolicy": "redirect-to-https", "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6"}, "Enabled": true }'
Why "serving from the edge" is such a genuinely large latency win, worth stating with the underlying reasoning, directly connecting to the TCP/networking fundamentals from the Linux & Networking series: physical distance to the speed of light is a real, unavoidable floor on round-trip latency — a user in Tokyo requesting content from an origin in us-east-1 pays for that full round trip on every request without a CDN; with CloudFront, only the FIRST request (a cache miss) pays that cost, and every subsequent request is served from a location physically near the user.
CloudFront Caching Behavior#
# Cache Policies control WHAT varies the cache key (query # strings, headers, cookies) — critical to configure correctly aws cloudfront create-cache-policy \ --cache-policy-config '{ "Name": "custom-cache-policy", "DefaultTTL": 86400, "MaxTTL": 31536000, "MinTTL": 1, "ParametersInCacheKeyAndForwardedToOrigin": { "EnableAcceptEncodingGzip": true, "QueryStringsConfig": {"QueryStringBehavior": "whitelist", "QueryStrings": {"Quantity": 1, "Items": ["version"]}}, "HeadersConfig": {"HeaderBehavior": "none"}, "CookiesConfig": {"CookieBehavior": "none"} } }' # Invalidate cached content when it changes before its TTL expires aws cloudfront create-invalidation \ --distribution-id E1AB2CD3EF4GH5 \ --paths "/static/app.css" "/index.html"
Why over-including cache-key parameters is a genuinely common, costly mistake worth naming explicitly: forwarding EVERY query string or header into the cache key (instead of only the ones that actually change the response) fragments the cache into many near-duplicate entries, dramatically reducing the cache HIT rate — the correct default is including ONLY the specific parameters that genuinely produce different content, exactly as the whitelist configuration above demonstrates.
CloudFront Origins — S3 and Custom Origins#
Diagram
A single distribution can route DIFFERENT path patterns to DIFFERENT origins — e.g. /static/* to an S3 bucket, everything else to an ALB fronting dynamic application servers — directly the same content-based routing philosophy already covered for ALB listener rules earlier in this part, just at the CDN layer.
CloudFront Origin Access Control — Locking Down S3 Origins#
A genuinely important security pattern worth knowing precisely, directly connecting to the S3 Block Public Access discussion from Part 5.
Diagram
aws cloudfront create-origin-access-control \ --origin-access-control-config '{"Name":"s3-oac","SigningProtocol":"sigv4","SigningBehavior":"always","OriginAccessControlOriginType":"s3"}' # The S3 bucket policy then explicitly trusts ONLY this # specific CloudFront distribution, not the public aws s3api put-bucket-policy --bucket app-static-assets --policy '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "cloudfront.amazonaws.com"}, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::app-static-assets/*", "Condition": {"StringEquals": {"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1AB2CD3EF4GH5"}} }] }'
Why this is worth adopting as the mandatory default whenever S3 sits behind CloudFront, worth stating explicitly, directly reinforcing the Block Public Access discipline from Part 5: without OAC, making an S3 bucket "public enough for CloudFront to read" also makes it public enough for ANYONE to read directly, bypassing CloudFront's caching, WAF protection (Part 9), and access logging entirely — OAC closes this gap completely, keeping the bucket genuinely private while still letting CloudFront serve its contents.
Route 53 — DNS as a Service#
Route 53 is AWS's DNS service — directly implementing the DNS fundamentals already covered in depth in the Linux & Networking series (Part 2), now as a fully managed, highly available, API-driven service.
# Create a hosted zone aws route53 create-hosted-zone --name example.com --caller-reference "$(date +%s)" # Create an A record pointing to an ALB (using an ALIAS record — # AWS-specific, works like a CNAME but at the zone apex, and # resolves for free with no extra DNS lookup) aws route53 change-resource-record-sets \ --hosted-zone-id Z1ABC2DEF3GHI \ --change-batch '{ "Changes": [{"Action": "UPSERT", "ResourceRecordSet": { "Name": "example.com", "Type": "A", "AliasTarget": {"HostedZoneId": "Z35SXDOTRQ7X7K", "DNSName": "app-alb-123456.us-east-1.elb.amazonaws.com", "EvaluateTargetHealth": true} }}] }'
Why an ALIAS record specifically at the zone apex is worth knowing as a genuinely AWS-specific detail, worth stating precisely: standard DNS (Linux & Networking series, Part 2) does not allow a CNAME record at a zone's apex/root (example.com itself, as opposed to www.example.com) — Route 53's ALIAS record type is a proprietary extension that behaves like a CNAME but IS allowed at the apex, and resolves without an extra DNS lookup (and without the usual per-query billing a CNAME-style record might incur), making it the standard way to point a bare domain at an AWS resource like an ALB or CloudFront distribution.
Route 53 Hosted Zones — Public and Private#
Diagram
aws route53 create-hosted-zone \ --name internal.example.com --caller-reference "$(date +%s)" \ --vpc VPCRegion=us-east-1,VPCId=vpc-0123456789abcdef0 \ --hosted-zone-config Comment="Private zone",PrivateZone=true
Route 53 Routing Policies#
Worth learning each precisely — this is genuinely one of the most commonly tested AWS DNS topics.
| Policy | Behavior | Best fit |
|---|---|---|
| Simple | One record, one (or a static set of) value(s) | A single-region, single-endpoint application |
| Weighted | Split traffic by PERCENTAGE across multiple endpoints | Canary deployments (Automation series, Part 1) at the DNS layer, gradual migration between two environments |
| Latency-based | Route to the endpoint with the LOWEST latency for the requester | A multi-region application optimizing for user-perceived speed |
| Geolocation | Route based on the USER's geographic location | Compliance-driven routing (e.g. EU users must reach an EU endpoint), localized content |
| Geoproximity | Like geolocation, but with an adjustable "bias" to shift traffic volume between regions | Fine-tuned regional traffic shaping |
| Failover | Primary/secondary — automatically shifts to the secondary if the primary fails a health check | Disaster Recovery series' failover patterns, implemented at the DNS layer |
| Multi-value answer | Returns SEVERAL healthy IPs, client picks one — a lightweight form of DNS-based load balancing | A simple, low-overhead alternative to a full load balancer for basic HA needs |
# A WEIGHTED record splitting 90% of traffic to v1, 10% to v2 - # directly implementing a canary deployment (Automation series) # at the DNS layer aws route53 change-resource-record-sets --hosted-zone-id Z1ABC2DEF3GHI --change-batch '{ "Changes": [{"Action": "UPSERT", "ResourceRecordSet": { "Name": "app.example.com", "Type": "A", "SetIdentifier": "v1", "Weight": 90, "TTL": 60, "ResourceRecords": [{"Value": "203.0.113.10"}] }}] }'
Route 53 Health Checks and DNS Failover#
Directly the DNS-layer implementation of the automated-failover concept already covered generically across the Reliability & Architecture Patterns and Disaster Recovery series.
aws route53 create-health-check --caller-reference "$(date +%s)" \ --health-check-config '{"IPAddress":"203.0.113.10","Port":443,"Type":"HTTPS","ResourcePath":"/healthz","RequestInterval":10,"FailureThreshold":3}' # A FAILOVER routing policy record pair — primary + secondary aws route53 change-resource-record-sets --hosted-zone-id Z1ABC2DEF3GHI --change-batch '{ "Changes": [{"Action": "UPSERT", "ResourceRecordSet": { "Name": "app.example.com", "Type": "A", "SetIdentifier": "primary", "Failover": "PRIMARY", "TTL": 60, "ResourceRecords": [{"Value": "203.0.113.10"}], "HealthCheckId": "abc123-health-check-id" }}] }'
Why DNS-based failover has a real, important limitation worth stating precisely, directly connecting to Part 4's Global Accelerator discussion: DNS changes are subject to TTL-based caching by clients and resolvers along the way — even after Route 53 itself detects a failure and updates its answer, some clients continue using the OLD, cached (now-unhealthy) IP until their local TTL expires, meaning DNS-based failover is never truly instantaneous. AWS Global Accelerator (Part 4) uses static anycast IPs specifically to avoid this exact DNS-propagation-delay limitation, for workloads where even that residual delay is unacceptable.
The Full Request Path, End to End#
Bringing this part together into one concrete, complete trace — genuinely worth being able to narrate this out loud in an interview.
Diagram
TLS/SSL Termination and ACM#
AWS Certificate Manager (ACM) provides free, auto-renewing TLS certificates, directly the concrete AWS implementation of the TLS discussion already covered in the Linux & Networking series (Part 2).
aws acm request-certificate \ --domain-name example.com --subject-alternative-names "*.example.com" \ --validation-method DNS # Attach the certificate to an ALB listener (TLS TERMINATES # at the load balancer — traffic from ALB to targets can be # plain HTTP within the private VPC, Part 4, or re-encrypted # for a stricter security posture) aws elbv2 create-listener \ --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/app-alb/abc123 \ --protocol HTTPS --port 443 \ --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xyz
Why "free and auto-renewing" is worth stating as a genuinely meaningful operational win, directly connecting to the Toil discussion from the SRE Fundamentals series: manually tracking certificate expiration dates and renewing them by hand is exactly the kind of repetitive, error-prone, well-defined process that toil automation should eliminate — an expired TLS certificate causing an outage is a genuinely common, entirely avoidable real-world incident, and ACM removes this failure mode almost entirely for AWS-hosted certificates.
A Full Worked Example: A Global, Multi-Region Web Application#
Bringing this entire part together into one concrete, realistic architecture.
Diagram
Walking through each layer's explicit reasoning: Route 53's latency-based routing sends each user to whichever region actually responds fastest for THEM, not a hardcoded geographic assumption; health checks on each region's endpoint mean Route 53 automatically stops routing to a region experiencing an outage, without any manual intervention; CloudFront in front of each region caches static content close to users regardless of which region ultimately serves them; Aurora Global Database (already covered in Part 6) keeps both regions' data synchronized with sub-second replication lag, so either region can legitimately serve reads (and, during a failover, be promoted to accept writes).
Why this design directly closes the loop with the Disaster Recovery series' Multi-Site Active-Active strategy, worth stating explicitly: this is precisely that strategy, expressed with concrete AWS services — Route 53 health-check-driven routing plus Aurora Global Database together implement automatic regional failover with a genuinely low RTO, at the real infrastructure cost the Disaster Recovery series already flagged as the honest tradeoff for this tier of protection.
Compute at the Edge: CloudFront Functions and Lambda@Edge#
Worth knowing both by name, and precisely when each fits, since they solve genuinely different scales of problem at the CDN layer.
Diagram
# CloudFront Function — extremely lightweight, extremely cheap, # runs on every single request at the edge aws cloudfront create-function \ --name add-security-headers \ --function-config '{"Comment":"Add security headers","Runtime":"cloudfront-js-2.0"}' \ --function-code fileb://function.js # Lambda@Edge — a real Lambda function (Part 7), deployed # to CloudFront's edge locations globally aws lambda publish-version --function-name resize-image-edge
Why CloudFront Functions are worth reaching for FIRST, before Lambda@Edge, for simple needs, worth stating explicitly as a genuine cost and performance tradeoff: CloudFront Functions execute in single-digit microseconds and cost a fraction of Lambda@Edge's price — for something as simple as adding a security header or rewriting a URL path, Lambda@Edge's fuller (and more expensive) capability is unnecessary overhead; reserve Lambda@Edge for logic that genuinely needs it (calling other AWS services, more complex conditional logic).
S3 Static Website Hosting Behind CloudFront#
A genuinely common, worth-knowing-precisely pattern combining Parts 5 and 8: serving an entire static website (a React/Vue single-page app, a static site generator's output) directly from S3, fronted by CloudFront.
# Configure S3 for static website hosting (though when using # CloudFront with OAC, as recommended earlier in this part, # the bucket itself stays PRIVATE — CloudFront serves the # website behavior, not S3's own public website endpoint) aws s3api put-bucket-website \ --bucket app-frontend --website-configuration '{"IndexDocument":{"Suffix":"index.html"},"ErrorDocument":{"Key":"error.html"}}' # For a Single-Page App (SPA), configure CloudFront's custom # error response to redirect 403/404s back to index.html, # letting the SPA's own client-side router handle the path aws cloudfront update-distribution --id E1AB2CD3EF4GH5 \ --distribution-config '{"CustomErrorResponses":{"Quantity":1,"Items":[{"ErrorCode":404,"ResponseCode":"200","ResponsePagePath":"/index.html","ErrorCachingMinTTL":10}]}}'
Why the SPA custom-error-response trick is worth knowing precisely, a genuinely common real gotcha: a Single-Page App's client-side router handles paths like /dashboard/settings entirely in the BROWSER — but S3, receiving a direct request for that exact object key, correctly returns a 404 since no such object exists. Configuring CloudFront to serve index.html (with a 200 status) for any 404 lets the SPA's own JavaScript router take over and render the correct view client-side, without this being a real broken link.
Load Balancing and CDN Best Practices — The Consolidated Checklist#
- Use Origin Access Control on every S3 origin behind CloudFront — never make the bucket itself public.
- Whitelist only the cache-key parameters that genuinely change the response — over-inclusion silently destroys the cache hit rate.
- Coordinate ASG lifecycle hook timeouts with target group deregistration delay — mismatched timers defeat graceful shutdown.
- Avoid sticky sessions where possible — externalize session state instead, for cleaner horizontal scaling.
- Use ACM for TLS certificates on anything AWS-hosted — free, auto-renewing, eliminates a common real cause of outages.
- Attach health checks to every Route 53 record used for failover or latency-based routing — a record without one can't automatically route around an unhealthy endpoint.
- Prefer CloudFront Functions over Lambda@Edge for simple logic (headers, redirects) — meaningfully cheaper and faster for what most edge logic actually needs.
- Use Global Accelerator, not DNS failover alone, when even residual TTL-based propagation delay is unacceptable.
Part 8 CLI Cheat Sheet#
| Area | Command | Purpose |
|---|---|---|
| ALB/NLB | aws elbv2 create-load-balancer | Create an ALB or NLB |
| Target groups | aws elbv2 create-target-group / register-targets | Define and populate a target group |
| Listeners | aws elbv2 create-listener / create-rule | Configure routing rules |
| CloudFront | aws cloudfront create-distribution | Create a CDN distribution |
| CloudFront | aws cloudfront create-invalidation | Force-refresh cached content |
| CloudFront | aws cloudfront create-origin-access-control | Lock an S3 origin to CloudFront only |
| Route 53 | aws route53 create-hosted-zone | Create a DNS zone (public or private) |
| Route 53 | aws route53 change-resource-record-sets | Create/update DNS records |
| Route 53 | aws route53 create-health-check | Create a health check for DNS failover |
| ACM | aws acm request-certificate | Request a free, auto-renewing TLS certificate |
Common Mistakes#
| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Making an S3 bucket public "so CloudFront can read it" | Also allows anyone to bypass CloudFront entirely, hitting S3 directly with no caching, WAF, or logging | Use Origin Access Control to keep the bucket private while still serving through CloudFront |
| Forwarding every query string/header into the CloudFront cache key | Fragments the cache into near-duplicate entries, tanking the hit rate | Whitelist only the specific parameters that genuinely change the response |
| Setting an ASG lifecycle hook timeout shorter than the target group's deregistration delay | The instance can terminate before the load balancer considers it fully drained | Coordinate both timeouts so the instance outlives the draining window |
| Relying on sticky sessions instead of externalizing session state | Makes safe scale-in and target replacement harder, working against horizontal scaling | Externalize session state (e.g. to ElastiCache, Part 6) and avoid stickiness where possible |
| Expecting DNS-based failover to be instantaneous | Client/resolver TTL caching means some clients keep using a stale, unhealthy IP for a while | Use Global Accelerator (Part 4) for failover needs where even residual DNS propagation delay is unacceptable |
| Manually tracking and renewing TLS certificates | Error-prone, and a common real cause of avoidable outages when a certificate expires unnoticed | Use ACM's free, auto-renewing certificates for anything AWS-hosted |
| Choosing an ALB for a workload needing a fixed, allowlist-able IP address | ALBs only expose a stable DNS name, not a fixed IP | Use an NLB with an Elastic IP per AZ for static-IP requirements |
Worked Practice Problems#
Problem 1: A team enables sticky sessions on their ALB to keep users on the same backend, and during a routine deployment (rolling update, Automation series), several users report being logged out or losing in-progress form data as their "sticky" target gets replaced. What's the underlying tension, and what's the more cloud-native fix?
Answer: Sticky sessions bind a user to a specific target for the duration of their session, but that target is not a permanent fixture — Auto Scaling (Part 3) and rolling deployments (Automation series) both routinely replace targets as a normal part of operation, and any user still "stuck" to a replaced target loses whatever server-local state (session data, form progress) lived only on that instance. The more cloud-native fix is externalizing session state entirely — storing it in ElastiCache (Part 6) rather than in the application server's local memory — so ANY target can serve ANY user's request statelessly, removing the need for sticky sessions altogether and making target replacement during deployments and scaling events fully transparent to users.
Problem 2: An application's S3-backed CloudFront distribution shows an unexpectedly low cache hit rate, and investigation reveals the cache policy forwards all query strings into the cache key, even though only a version parameter actually changes the served content — every other query string parameter (tracking IDs, session tokens appended by various referrers) is incidental and doesn't affect the response. What's happening, and what's the fix?
Answer: Because the cache key includes every query string parameter, requests that are functionally identical (same actual content) but carry different incidental tracking parameters are treated as entirely DIFFERENT cache entries — fragmenting what should be one highly-reused cache entry into many rarely-reused ones, tanking the overall hit rate. The fix is narrowing the cache policy's query string behavior to a whitelist containing only version — the one parameter that genuinely changes the response — so functionally identical requests with different incidental tracking parameters correctly collapse onto the same cache entry.
Problem 3: A global application currently uses a Simple routing policy in Route 53, pointing to a single region's ALB. As the company expands to a second region for lower latency to European users, the team wants traffic to automatically route each user to whichever region is actually faster for them, and to automatically stop sending traffic to either region if it becomes unhealthy. What Route 53 configuration achieves both goals simultaneously?
Answer: A Latency-based routing policy, with a Route 53 health check attached to each region's record. Latency-based routing directly satisfies the first goal — Route 53 measures actual network latency between AWS regions and resolver locations, routing each user to whichever configured region genuinely responds fastest for them, rather than a hardcoded geographic assumption. Attaching a health check to each region's record satisfies the second goal — Route 53 automatically excludes an unhealthy region's record from its answers, meaning even if that region happens to have the lowest latency for a particular user, traffic is still correctly routed to the remaining healthy region instead, achieving both latency optimization and automatic failover in a single configuration.
Problem 4: A single-page application (built with a client-side JavaScript router) is hosted on S3 and served through CloudFront. Users report that directly loading (or refreshing) a deep link like /dashboard/settings shows a broken "404 Not Found" page, even though clicking through to that same page from within the app works perfectly. What's causing this, and what CloudFront configuration fixes it?
Answer: The client-side router only handles navigation that happens IN THE BROWSER, after the initial page (and its JavaScript bundle) has already loaded — a direct request or page refresh for /dashboard/settings instead goes straight to S3 asking for an object with that exact key, which genuinely doesn't exist (only index.html and the app's static assets actually exist as S3 objects), correctly producing a 404 from S3's perspective. The fix is configuring a CloudFront custom error response that catches 403/404 errors and serves index.html instead, with an HTTP 200 status — this lets the app's JavaScript bundle load as normal, at which point the client-side router reads the URL path and renders the correct view itself, resolving what looked like a broken link into a correctly working deep link.
Problem 5: A team needs to add a simple security header to every response served by their CloudFront distribution, and is deciding between writing a CloudFront Function or a Lambda@Edge function to accomplish this. Which would you recommend, and why?
Answer: A CloudFront Function, not Lambda@Edge, for this specific need. Adding a static security header is exactly the kind of simple, lightweight logic CloudFront Functions are purpose-built for — they execute in single-digit microseconds and cost meaningfully less than Lambda@Edge, since they run directly within CloudFront's own execution environment rather than invoking a full Lambda runtime at the edge. Lambda@Edge's additional capabilities (calling other AWS services, more complex conditional branching) would be complete overkill for a task this simple, and would needlessly add both cost and a small amount of latency per request compared to the CloudFront Function alternative — the right tool should always match the actual complexity of the task, not default to the more powerful option out of habit.
Summary and What's Next#
- ALB (Layer 7) enables content-based routing (path/host) and is the natural home for canary/blue-green traffic splitting; NLB (Layer 4) provides extreme performance and static IPs for non-HTTP or allowlist-driven needs.
- Target group health checks are the same mechanism an Auto Scaling Group's ELB health check type (Part 3) relies on — closing the loop between traffic routing and self-healing capacity.
- CloudFront serves cached content from edge locations close to users, dramatically reducing round-trip latency; Origin Access Control is mandatory when an S3 origin sits behind it, to prevent bypassing the CDN entirely.
- Route 53 provides DNS as a fully managed service, with routing policies (Simple, Weighted, Latency-based, Geolocation, Failover, Multi-value) each solving a distinct real traffic-management need.
- DNS-based failover has an inherent TTL-caching delay — Global Accelerator (Part 4) exists specifically for failover needs where even this residual delay is unacceptable.
- ACM provides free, auto-renewing TLS certificates, eliminating a genuinely common, avoidable real-world cause of outages (expired certificates).
- A global, multi-region architecture combining Route 53 latency-based routing, CloudFront, and Aurora Global Database (Part 6) is the concrete AWS realization of the Disaster Recovery series' Multi-Site Active-Active strategy.
Continue to Part 9 (09-security-and-compliance.md) for a comprehensive, in-depth look at AWS's security services — encryption, threat detection, compliance tooling, and network security — building directly on the networking and identity foundations from Parts 2 and 4.