AWS Intermediate #6: ALB / NLB and ACM (HTTPS)

10 min read

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:

ProblemWhat an LB solves
EC2 dies and the site is downHealth checks remove the dead, route to the live
Traffic too high for one machineDistribute load across many instances
AZ outage takes everything downSpread Multi-AZ
HTTPS cert per instance is a painTerminate TLS at the LB once
Canary / Blue-GreenListener 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 layerL7 (HTTP / HTTPS)L4 (TCP / UDP / TLS)L3/L4 (IP)
Routingpath / host / header / methodport onlypacket pass-through
ThroughputGoodVery high (millions RPS)Fast in its niche
WebSocket-
HTTP/2✅ (TLS)-
Static IP❌ (DNS)✅ (EIP per AZ)-
WAF-
Cognito auth-
Lambda target-
UseWeb / APIGames / IoT / TCP / gRPCSecurity appliances (firewalls)

Decision guide #

LB decision tree
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 → GWLB

99% of normal web workloads use ALB. NLB is for games / IoT / extreme throughput / when a static IP is required.

Shape of an ALB #

ALB structure
                    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).

Create ALB + Listener
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.

Create a Target Group
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 3

target-type options:

TypeUse
instanceEC2 instance ID. Smooth for SG-by-SG
ipArbitrary IP (in VPC). Auto-registration for ECS / Fargate
lambdaLambda function. ALB → Lambda pattern

3) Listener Rule — routing rules #

In the same Listener, route to different Target Groups by path / host / header.

Shape of Listener Rules
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-web

Rules 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
Canonical HTTP → HTTPS redirect
Listener (port 80)
  default action: redirect → HTTPS://#{host}#{path}#{query} (301)

Listener (port 443)
  default action: forward → tg-web

This 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.

Health Check flow
ALB ─ HTTP GET /health ──▶ EC2:8080
                       200 ──▶ healthy (3 in a row)
                       5xx ──▶ unhealthy (3 in a row) → removed from routing

Common options:

OptionDescription
HealthCheckPathLight paths like /health, /healthz
HealthCheckProtocolHTTP / HTTPS
HealthCheckIntervalSeconds30 (typical)
HealthyThresholdCount2–3 in a row 200 → healthy
UnhealthyThresholdCount2–3 in a row failure → unhealthy
Matcher.HttpCode200 or 200-299

Designing the health-check path #

A good /health response:

Light health endpoint (FastAPI)
@app.get("/health")
def health():
    return {"status": "ok"}

Deep health that checks DB, etc., goes on a separate path:

Deep health
@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.

Enable stickiness
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=86400

Cases:

  • 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
NLB + TLS Listener
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 #

ACM cert request
aws acm request-certificate \
  --domain-name example.com \
  --subject-alternative-names "*.example.com" "api.example.com" \
  --validation-method DNS \
  --region ap-northeast-2

Validation 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).

Region mapping
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.

Steps to enable HTTPS
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 Alias

These 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.

Common policies
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 enforced

If 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.

Connection Draining
ALB ─ no new requests ─▶ EC2  (deregistration_delay = 300s)
        in-flight finish

Default 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 ↔ EC2 SG
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: all

NLB 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.com zone instead of example.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.

X