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:
99
docs/deploy/aws.md
Normal file
99
docs/deploy/aws.md
Normal 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.
|
||||
Reference in New Issue
Block a user