AWS Intermediate #6: ALB / NLB and ACM (HTTPS)
What the #5 Route 53 domain points to almost always has a load balancer sitting in front of it. AWS’s family of managed load balancers is collectively ELB (Elastic Load Balancing), with three flavors inside: ALB / NLB / GWLB.
In this post we line up the differences between the three → ALB’s Listener / Target Group / Health Check flow → how ACM issues a cert and turns on HTTPS in one shot.
What load balancers solve #
Pointing a domain straight at a single EC2 breaks in these ways:
| Problem | What an LB solves |
|---|---|
| EC2 dies and the site is down | Health checks remove the dead, route to the live |
| Traffic too high for one machine | Distribute load across many instances |
| AZ outage takes everything down | Spread Multi-AZ |
| HTTPS cert per instance is a pain | Terminate TLS at the LB once |
| Canary / Blue-Green | Listener Rule splits ratios |
Production almost always uses Internet → ALB / NLB → EC2 / ECS / Lambda.
ALB / NLB / GWLB — three options compared #
| ALB (Application LB) | NLB (Network LB) | GWLB (Gateway LB) | |
|---|---|---|---|
| OSI layer | L7 (HTTP / HTTPS) | L4 (TCP / UDP / TLS) | L3/L4 (IP) |
| Routing | path / host / header / method | port only | packet pass-through |
| Throughput | Good | Very high (millions RPS) | Fast in its niche |
| WebSocket | ✅ | ✅ | - |
| HTTP/2 | ✅ | ✅ (TLS) | - |
| Static IP | ❌ (DNS) | ✅ (EIP per AZ) | - |
| WAF | ✅ | ❌ | - |
| Cognito auth | ✅ | ❌ | - |
| Lambda target | ✅ | ❌ | - |
| Use | Web / API | Games / IoT / TCP / gRPC | Security appliances (firewalls) |
Decision guide #
HTTP(S)?
├── YES → ALB
│ └── Very high RPS (hundreds of thousands+)? → ALB → NLB if it caps out
└── NO →
TCP/UDP?
├── YES → NLB
└── Pass through external security appliance → GWLB99% of normal web workloads use ALB. NLB is for games / IoT / extreme throughput / when a static IP is required.
Shape of an ALB #
Route 53
│
▼
┌────────┐
│ ALB │
│ │
└─┬──┬───┘
│ │
Listener (port 443)
│ │
▼ ▼
Listener Rules
(path / host)
│
├── /api/* ────▶ Target Group A (api EC2s)
├── /admin/* ────▶ Target Group B (admin)
└── default / ────▶ Target Group C (web)
│
├── EC2 #1 (AZ a)
├── EC2 #2 (AZ b)
└── EC2 #3 (AZ a)Three core parts:
1) Listener — receiving port #
What port the ALB receives on. Usually 80 (HTTP), 443 (HTTPS).
aws elbv2 create-load-balancer \
--name my-alb \
--subnets subnet-pubA subnet-pubB \
--security-groups sg-alb \
--scheme internet-facing \
--type application
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=<acm-arn> \
--default-actions Type=forward,TargetGroupArn=<tg-arn>2) Target Group — destinations #
A logical grouping of instances, IPs, or Lambda functions that the Listener forwards traffic to. Each group has its own port and protocol.
aws elbv2 create-target-group \
--name my-app-tg \
--protocol HTTP \
--port 8080 \
--vpc-id vpc-... \
--target-type instance \
--health-check-protocol HTTP \
--health-check-path /health \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3target-type options:
| Type | Use |
|---|---|
instance | EC2 instance ID. Smooth for SG-by-SG |
ip | Arbitrary IP (in VPC). Auto-registration for ECS / Fargate |
lambda | Lambda function. ALB → Lambda pattern |
3) Listener Rule — routing rules #
In the same Listener, route to different Target Groups by path / host / header.
Priority Condition Action
10 host = api.example.com forward → tg-api
20 path = /admin/* forward → tg-admin
30 path = /static/* redirect → cloudfront.example.com
default * forward → tg-webRules evaluate by priority (lower first). Stops on match.
Listener Rule actions #
- forward — to a Target Group (with weights for multiple)
- redirect — to another URL (the canonical HTTP → HTTPS redirect)
- fixed-response — fixed reply (maintenance page)
- authenticate-cognito / -oidc — pass through after user auth
Listener (port 80)
default action: redirect → HTTPS://#{host}#{path}#{query} (301)
Listener (port 443)
default action: forward → tg-webThis pattern is the production standard — every port-80 request is permanently sent to 443.
Health Check — only live targets #
The Target Group’s Health Check drops dead instances automatically and re-adds them when they recover.
ALB ─ HTTP GET /health ──▶ EC2:8080
│
200 ──▶ healthy (3 in a row)
5xx ──▶ unhealthy (3 in a row) → removed from routingCommon options:
| Option | Description |
|---|---|
HealthCheckPath | Light paths like /health, /healthz |
HealthCheckProtocol | HTTP / HTTPS |
HealthCheckIntervalSeconds | 30 (typical) |
HealthyThresholdCount | 2–3 in a row 200 → healthy |
UnhealthyThresholdCount | 2–3 in a row failure → unhealthy |
Matcher.HttpCode | 200 or 200-299 |
Designing the health-check path #
A good /health response:
@app.get("/health")
def health():
return {"status": "ok"}Deep health that checks DB, etc., goes on a separate path:
@app.get("/health/deep")
def deep_health(db: Session = Depends(get_db)):
db.execute("SELECT 1")
return {"status": "ok", "db": "ok"}Always keep /health light, and use /health/deep from monitoring / debugging. ALB hammering deep health every 30s burdens the DB.
Sticky session — same target every time #
The option to send a particular user’s requests to the same instance every time.
aws elbv2 modify-target-group-attributes \
--target-group-arn <tg-arn> \
--attributes \
Key=stickiness.enabled,Value=true \
Key=stickiness.type,Value=lb_cookie \
Key=stickiness.lb_cookie.duration_seconds,Value=86400Cases:
- Old apps that keep session in memory — almost shouldn’t, but a temporary measure during migration
- Long-lived connections like WebSocket — automatic stickiness
The canonical production answer is session stored in an external store (Redis / DB) with stateless application instances. Stickiness is a last resort.
Where NLB sits #
NLB handles what ALB cannot.
NLB strengths #
- Static IP (EIP per AZ) — for firewall whitelisting
- Millions of RPS
- TLS termination too (NLB unwraps TLS, sends plain to backend)
- PrivateLink support (expose a service to another VPC)
NLB weaknesses #
- L4 only — no path / host / header routing
- No direct WAF integration
- One Listener has one Target Group
aws elbv2 create-load-balancer \
--name my-nlb --type network \
--subnets subnet-pubA subnet-pubB
aws elbv2 create-listener \
--load-balancer-arn <nlb-arn> \
--protocol TLS --port 443 \
--certificates CertificateArn=<acm-arn> \
--default-actions Type=forward,TargetGroupArn=<tg-arn>ACM — the certificate-issuance service #
ACM (AWS Certificate Manager) issues and auto-renews public SSL certs at no charge for use with ALB / NLB / CloudFront / API Gateway.
Request a cert #
aws acm request-certificate \
--domain-name example.com \
--subject-alternative-names "*.example.com" "api.example.com" \
--validation-method DNS \
--region ap-northeast-2Validation methods:
- DNS validation (recommended) — auto-verified via Route 53 CNAME, auto-renewed
- Email validation (legacy) — emails admin@example.com, manual yearly
Almost always DNS validation. With Route 53 alongside, the console offers a “Create record in Route 53” button.
Certs — region matching rule #
ACM certs are per region. ALB in Seoul → cert in Seoul. But CloudFront is always us-east-1 (#7).
ALB (Seoul) → ACM cert (ap-northeast-2)
NLB (Tokyo) → ACM cert (ap-northeast-1)
CloudFront → ACM cert (us-east-1) ← always
API Gateway (REST) → ACM cert (that region)
API Gateway (Edge) → ACM cert (us-east-1)Auto-renewal #
ACM certs are valid 13 months by default, auto-renewed starting 60 days before expiry:
- DNS-validated certs: fully automatic
- Email-validated: manual (so DNS is recommended)
When auto-renewal fails:
- DNS validation CNAME removed → resolution fails
- ALB still uses the cert but the domain moved → validation fails
ACM console sends expiry alerts automatically; CloudWatch alarms are also possible.
One-line procedure to enable HTTPS #
Assume you already have a domain and an ALB.
1. ACM request a cert (DNS validation)
2. Add validation CNAME in Route 53 (one console click)
3. Wait for ISSUED (a few minutes)
4. Attach the cert to ALB Listener 443
5. Listener 80 → HTTPS redirect
6. Route 53 domain → ALB AliasThese six steps are the production standard. You don’t have to renew yearly either.
Security Policy — TLS versions #
The Listener’s Security Policy defines allowed TLS versions / ciphers.
ELBSecurityPolicy-TLS13-1-2-2021-06 ← recommended (TLS 1.3, 1.2)
ELBSecurityPolicy-TLS-1-2-2017-01 ← good compatibility
ELBSecurityPolicy-FS-2018-06 ← Forward Secrecy enforcedIf you want to drop old clients (TLS 1.0, 1.1), use TLS13-1-2-2021-06. New policies arrive yearly — review periodically.
Connection Draining — graceful shutdown #
The feature that drains in-flight requests gracefully when an instance is manually deregistered or scaled in by ASG.
ALB ─ no new requests ─▶ EC2 (deregistration_delay = 300s)
↑
in-flight finishDefault 300s. 30–60s is usually enough. Too short = in-flight cut; too long = slow deploys.
LB SG and EC2 SG #
The SG pattern from #2:
ALB SG (sg-alb)
Inbound: 443 ← 0.0.0.0/0
Outbound: all
EC2 SG (sg-app)
Inbound: 8080 ← sg-alb ← the ALB SG itself
Outbound: allNLB differs in that legacy NLBs have no SG; newer NLBs support one SG. The EC2 SG sees the client’s IP directly rather than the NLB’s IP. In production, when you need to restrict client IPs through an NLB, you sometimes add NACL or other VPC-level controls.
Common pitfalls #
1) ACM cert won’t ISSUED #
Almost always 99%:
- Validation CNAME not in Route 53
- CNAME in the wrong zone (in
api.example.comzone instead ofexample.com) - TTL too long, no propagation → wait ~5 min
2) CloudFront cert created in Seoul #
CloudFront is us-east-1 only. Switch the console to N. Virginia.
3) No HTTP 80 Listener, only HTTPS #
Users hitting http://example.com time out. Redirect to HTTPS in the 80 Listener is the standard.
4) Health check path is / #
A / that’s heavy (DB calls, etc.) gets bombed every 30s by ALB. Make a separate light path like /health.
5) Target Group port vs Listener port #
ALB Listener is 443 but Target Group EC2 might be 8080. They are separate. The TG port is the port the EC2 listens on.
6) 502 Bad Gateway #
ALB ↔ EC2 response broken. Common causes:
- EC2’s keep-alive timeout < ALB idle timeout (60s default)
- EC2 cuts before responding
- EC2 SG doesn’t accept ALB SG inbound
- EC2 not listening on 8080
7) Sticky session causing skew #
One instance gets all the requests, CPU 100% — turn off stickiness, go stateless.
8) Cross-Zone off #
When the legacy ALB cross-zone option is off, load is split per AZ — one AZ has fewer instances but the same traffic share → skew. Turn on Cross-zone load balancing (default on for ALB, off for NLB due to cost).
9) Idle timeout trap #
ALB idle timeout (60s default) shorter than backend / client keep-alive → 502. Usually backend keep-alive > ALB idle.
Wrap-up #
What we took home this time:
- ELB = ALB / NLB / GWLB. 99% normal web is ALB
- ALB: L7, path / host / header routing, WAF integration, Lambda target, Cognito auth
- NLB: L4, static IP, millions RPS, PrivateLink
- ALB structure = Listener (incoming port) → Listener Rule (path/host) → Target Group (destination) → Health Check
- Target Group target-type = instance / ip / lambda
- Listener Rule action = forward / redirect / fixed-response / authenticate
- HTTP → HTTPS redirect is the production standard (default action of port-80 Listener)
- Health check uses a light
/health, deep on a separate/health/deep - ACM = free SSL cert + auto-renew. DNS validation recommended
- Region mapping — ALB same region, CloudFront always
us-east-1 - 6-step HTTPS — request cert → CNAME validate → ISSUED → Listener attach → 80 redirect → Route 53 Alias
- Security Policy restricts allowed TLS versions
- Pitfalls — cert validation fails, CloudFront region, missing 80, heavy health check, port confusion, 502, sticky skew, cross-zone, idle timeout
Next — CloudFront #
The ALB piece is set. Last, the thing that caches close to users — CloudFront.
In #7 CloudFront for static site delivery we’ll line up the Origin / Behavior / Cache Policy flow, the S3 + CloudFront static-hosting pattern, OAC for safe S3 access, and invalidation.