docs(deploy): aws.md — ALB appendix: three mandatory overrides, never NLB-TLS (slice-G)

Docs-only appendix per ADR-0011 (never a shipped artifact). The three
mandatory ALB attribute overrides (idle_timeout=4000,
client_keep_alive=604800, preserve_host_header=true) with the telephony
reasons; the never-NLB-TLS rule with the 350s-silent-drop + no-HTTP-
context rationale; 100-target-group fleet ceiling; per-node EIPs for
direct media UDP; copy-paste aws elbv2 CLI path to first call.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
This commit is contained in:
2026-07-05 22:47:46 -04:00
parent 33c3a145b0
commit a0b3e248f5

99
docs/deploy/aws.md Normal file
View File

@@ -0,0 +1,99 @@
# AWS appendix — ALB + ACM (docs only, never a shipped artifact)
Scope: operators **already on EC2**. There, an ALB is trust-neutral (the engine is on AWS
infrastructure regardless) and ACM renewal is genuinely zero-touch. This page is an appendix,
not a recommendation to move to AWS — the shipped edges are Caddy
([topologies.md](topologies.md)) and BYO proxies ([reverse-proxies.md](reverse-proxies.md)).
All ALB/NLB facts below are grounded in the [TLS/edge decision brief §3(c)](../superpowers/specs/2026-07-05-tls-edge-decision-brief.md).
## Never NLB-TLS
Do not put the engine behind an NLB TLS listener. Two disqualifiers, ratified in
[ADR-0011](../adr/0011-deployment-topology.md):
1. **Fixed 350-second idle timeout** that silently drops flows — any quiet WebSocket (hold,
parked call, unidirectional stream) dies mid-call with no close frame.
2. **No HTTP context**: an NLB adds no `X-Forwarded-Proto/Host`, so the engine cannot
reconstruct the public URL that `X-Twilio-Signature` is HMAC'd over
([Twilio's SSL-termination guidance](https://www.twilio.com/en-us/blog/developers/tutorials/building-blocks/handle-ssl-termination-twilio-node-js-helper-library))
— genuine traffic fails validation.
## The three mandatory ALB attribute overrides
The defaults kill telephony. All three overrides are **mandatory**, not tuning:
| Attribute | Value | Why |
|---|---|---|
| `idle_timeout.timeout_seconds` | `4000` | The default 60 s kills any quiet WS. 4000 is the ALB maximum — still finite, so the engine's app-level WS pings (`RUTSTER_WS_PING_SECS`, default 20) remain load-bearing here. |
| `client_keep_alive.seconds` | `604800` | The 3600 s default can terminate the client connection under a **>1-hour call**. Set to the 7-day maximum. |
| `routing.http.preserve_host_header.enabled` | `true` | Signature validation reconstructs the URL *as Twilio saw it* — the engine must receive the honest public `Host`, and its `RUTSTER_TRUSTED_PROXIES` must include the ALB's subnet CIDRs. |
## Copy-paste path (CLI)
Assumes: engine node(s) running per [quickstart-docker.md](quickstart-docker.md) with the
`caddy` service disabled (plaintext `:8080`), a VPC, two subnets, and a security group
allowing `:443` from anywhere and `:8080` from the ALB.
```bash
VPC=vpc-0123456789abcdef0
SUBNETS="subnet-0aaa subnet-0bbb"
SG=sg-0ccc
ALB_ARN=$(aws elbv2 create-load-balancer --name rutster-edge --type application \
--subnets $SUBNETS --security-groups $SG \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
# THE THREE MANDATORY OVERRIDES — do not skip:
aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" --attributes \
Key=idle_timeout.timeout_seconds,Value=4000 \
Key=client_keep_alive.seconds,Value=604800 \
Key=routing.http.preserve_host_header.enabled,Value=true
TG_ARN=$(aws elbv2 create-target-group --name rutster-node-1 \
--protocol HTTP --port 8080 --vpc-id "$VPC" \
--health-check-path /readyz \
--query 'TargetGroups[0].TargetGroupArn' --output text)
aws elbv2 register-targets --target-group-arn "$TG_ARN" --targets Id=i-0node1
CERT_ARN=$(aws acm request-certificate --domain-name '*.pbx.example.com' \
--validation-method DNS --query CertificateArn --output text)
# Create the DNS validation CNAME ACM reports, wait for ISSUED, then:
LISTENER_ARN=$(aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" \
--protocol HTTPS --port 443 \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--certificates CertificateArn="$CERT_ARN" \
--default-actions Type=forward,TargetGroupArn="$TG_ARN" \
--query 'Listeners[0].ListenerArn' --output text)
# Fleet growth: one target group + one host-header rule per node
# (node-addressed placement — ADR-0011):
aws elbv2 create-rule --listener-arn "$LISTENER_ARN" --priority 10 \
--conditions Field=host-header,Values=node-1.pbx.example.com \
--actions Type=forward,TargetGroupArn="$TG_ARN"
```
Point `*.pbx.example.com` (alias record) at the ALB. First call: identical to
[quickstart-docker.md](quickstart-docker.md) — browser at your domain, Twilio webhook at
`https://pbx.example.com/v1/trunk/webhook`.
The `--ssl-policy` above serves TLS 1.2+1.3 — never pick a 1.3-only policy (Twilio's TLS 1.3
client support is undocumented; see the invariants in
[certificates.md](certificates.md)).
## WebRTC media does not ride the ALB
The ALB carries HTTPS/WSS only. WebRTC media UDP goes **direct to each node**: give every
engine node an Elastic IP, open the media UDP range in the node's security group, and set
`RUTSTER_MEDIA_ADVERTISED_IP` to that EIP.
## Caps and traps
- **Hard cap: 100 target groups per ALB.** With one TG per node (host-header routing), that
is your fleet ceiling per ALB.
- **AWS fleet rotation can still cut a multi-hour call** — ALB infrastructure is replaced
under maintenance without regard for your WS lifetimes. Accepted residual risk; there is no
attribute for it.
- Health checks target `/readyz` (503 while draining or at the admission cap) — scale-in must
be drain-then-terminate: deregister the target, wait out
`RUTSTER_DRAIN_DEADLINE_SECS`, then terminate the instance.