diff --git a/docs/deploy/aws.md b/docs/deploy/aws.md new file mode 100644 index 0000000..b9c16c3 --- /dev/null +++ b/docs/deploy/aws.md @@ -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.