Files
rutster/docs/deploy/aws.md
Aaron D. Lee a0b3e248f5 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>
2026-07-05 22:47:46 -04:00

5.1 KiB

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) and BYO proxies (reverse-proxies.md). All ALB/NLB facts below are grounded in the TLS/edge decision brief §3(c).

Never NLB-TLS

Do not put the engine behind an NLB TLS listener. Two disqualifiers, ratified in ADR-0011:

  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) — 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 with the caddy service disabled (plaintext :8080), a VPC, two subnets, and a security group allowing :443 from anywhere and :8080 from the ALB.

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

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.