Sistemati path API /v1, AppArmor/auth MediaMTX sul nodo e defaults nbg1 così lo smoke Cloud è ripetibile. Co-authored-by: Cursor <cursoragent@cursor.com>
17 KiB
Autoscale streaming — Proxmox attuale + Hetzner Cloud
Stato: fasi 0–4 implementate sul branch — solo test lab; nessun deploy produzione
Branch: feature/streaming-autoscale-hetzner
Ultimo aggiornamento: 2026-08-09 (Fase 4 hardening + runbook)
Sostituisce / estende: il piano overflow-only HETZNER_CLOUD_RELAY_OVERFLOW.md. Questo documento copre MediaMTX + ffmpeg, warm spare, DNS, storage, disco e lab sul Proxmox di produzione.
Documenti correlati: ARCHITECTURE.md, LIVE_STREAMING.md, REPLAY_MODULE.md, OPS_MONITORING.md.
1. Obiettivo
Scalare molte dirette contemporanee (sito e/o YouTube) senza saturare un unico box:
- Control plane (ora): Proxmox già in produzione (
192.168.1.146//opt/matchlivetv) — sito, API, DB, Redis, Garage, MediaMTX/ffmpeg “home”. - Data plane elastico: Hetzner Cloud — nodi
MediaMTX + ffmpegcon warm spare (+1). - DNS ingest: secondo dominio su Hetzner DNS (API).
- Lab: stesso Proxmox, con
ProxmoxLabProvider, prima di affidarsi al Cloud in produzione. - Futuro (non bloccante): migrazione control plane su Hetzner Auction + Proxmox (vSwitch nativo, hardware dedicato).
2. Decisioni chiuse
| # | Decisione | Scelta |
|---|---|---|
| D1 | Host primario ora | Proxmox già usato in produzione (non attendere Auction) |
| D1b | Host primario futuro | Hetzner Server Auction + Proxmox (quando si migra) |
| D2 | Overflow / warm spare | Hetzner Cloud (attivato per questo progetto) |
| D3 | Unità di scala | Nodo stream = MediaMTX + worker youtube_relay (ffmpeg remux copy) |
| D4 | Warm pool | Almeno 1 spare ready quando il carico si avvicina al cap |
| D5 | DNS ingest | Dominio mltv-stream.net (registrar Aruba, NS → Hetzner DNS). matchlivetv.it resta su Aruba intatto |
| D6 | Aruba | Nessuna delega NS; niente Floating IP prenotate |
| D7a | Rete ora (casa/Proxmox ↔ Cloud) | WireGuard (o Tailscale) privato: Redis, DB, API MediaMTX home. Non esporre Redis/Postgres su WAN |
| D7b | Rete futuro (Auction ↔ Cloud) | vSwitch Robot + Cloud Network (stessa location) |
| D8 | Object storage | Garage resta. B2 = migrazione pronta quando misurato |
| D9 | Staging | Segmenti su disco VM stream-* in live; upload Garage a fine sessione |
| D10 | Prodotto | Consigliare YouTube per alleggerire storage/egress replay |
| D11 | Disco | Separazione volumi + monitoraggio attivo obbligatorio (§6) |
| D12 | Lab prima del Cloud prod | ProxmoxLabProvider + DNS lab sullo stesso Proxmox; poi HetznerCloudProvider |
3. Topologia — fase attuale (Proxmox prod + Cloud)
Internet
│ HTTPS :443 · RTMP :1935
▼
┌──────────────────────────────────────────────────────────┐
│ Proxmox produzione (attuale) │
│ eminux@192.168.1.146 · /opt/matchlivetv │
│ │
│ edge · rails · sidekiq-core · postgres · redis │
│ garage · stream-home (MediaMTX + youtube_relay) │
│ autoscaler (Rails/Sidekiq) │
│ │
│ Lab (opz.): clone VM stream-* via ProxmoxLabProvider │
└────────────────────────┬─────────────────────────────────┘
│ WireGuard / tunnel privato
▼
┌──────────────────────────────────────────────────────────┐
│ Hetzner Cloud — pool stream nodes │
│ stream-01 … stream-N MediaMTX + ffmpeg │
│ + 1 spare ready (warm) │
└────────────────────────┬─────────────────────────────────┘
│
Hetzner DNS (secondo dominio)
ingest-XX.<dominio> → IP pubblico nodo
Futuro Auction: stesso schema, tunnel sostituito da vSwitch; control plane migrato sul bare metal Hetzner.
Principio: Rails solo controllo. Live sui MediaMTX del nodo assegnato. Replay su Garage (poi eventualmente B2).
4. DNS (secondo dominio)
RTMP non tollera round-robin su un unico hostname.
- Dominio
mltv-stream.netregistrato su Aruba; zona DNS su Hetzner Console (progettomatchlivetv-stream). - NS Aruba del dominio →
hydrogen/oxygen/heliumHetzner (delegazione OK). - Autoscaler a VM ready → upsert
Aingest-03.mltv-stream.net→ health-check → nodoready. - API create/start sessione → URL del nodo assegnato:
rtmp://ingest-03.mltv-stream.net:1935/live/match_<uuid>
https://ingest-03.mltv-stream.net/hls/... # o via edge matchlivetv.it che proxya il nodo
- Scale-in: drain → delete DNS → destroy VM.
Hostname pubblici dei nodi vivono sotto *.mltv-stream.net. In lab: LabDnsProvider (/etc/hosts, dnsmasq).
5. Autoscaler e warm spare
Ogni nodo: max_publishers / max_relays (es. 4).
| Regola | Comportamento |
|---|---|
| Scale-out | free_slots sotto soglia soft → crea 1 nodo |
| Warm spare | Se spare_ready < 1 vicino al carico → crea spare |
| Scale-in | Idle da X min e non unica spare → destroy |
| Floor | stream-home sul Proxmox sempre on |
CloudProvider: HetznerCloudProvider (prod overflow) + ProxmoxLabProvider (test).
Code Sidekiq: coda youtube_relay isolata; sticky owner Redis.
6. Disco Proxmox — requisito critico
Non mettere OS, DB e video sullo stesso volume che può riempirsi al 100%.
Vale già sul Proxmox di produzione (rinforzare ora), e di nuovo alla migrazione Auction.
6.1 Layout volumi (target)
| Volume / disco | Contenuto | Note |
|---|---|---|
os |
Proxmox / sistema host | Quasi statico |
vm-system / root stack |
rails, edge, redis, … | Snapshot |
vm-data-db |
Postgres | Separato; backup prioritari |
vm-data-video |
Garage + staging MediaMTX | Disco che può riempirsi |
backup |
Backup / PBS | Mai sullo stesso disco dei video |
Regole: cap Garage/staging; se staging pieno → rifiutare nuove sessioni, non far cadere DB/API.
6.2 Monitoraggio attivo (obbligatorio)
| Metrica | Warn | Critical |
|---|---|---|
Uso % volume video |
≥ 70% | ≥ 85% |
| Spazio libero GB | sotto soglia | < X GB |
| Inode liberi | bassi | esaurimento |
| Crescita GB/giorno Garage | anomalia | — |
| Purge/retention falliti | — | immediato |
Staging per nodo stream-* |
≥ 70% | ≥ 85% + blocco live su quel nodo |
Checklist:
- Alert ntfy disco testati
- Vista ops “disco video”
- Test periodico “80% pieno”
- Restore di prova Postgres
7. Storage: Garage ora, B2 dopo
| Ora | Garage | | Dopo | Backblaze B2 quando metriche/€ lo giustificano |
Client S3-ready; staging locale invariato. Spinta prodotto YouTube riduce pressione replay.
8. Prodotto: YouTube consigliato
Suggerire YouTube in app/dashboard quando ha senso → meno storage/egress MatchLiveTV; middle-tier (slate + relay) resta il valore. Live solo-sito restano supportate.
9. Astrazioni codice
CloudProvider
create_node / destroy_node / list_nodes / wait_until_running / public_ip
→ HetznerCloudProvider | ProxmoxLabProvider
DnsProvider
upsert_a / delete_a
→ HetznerDnsProvider | LabDnsProvider
StreamNodeRegistry
register / mark_ready / allocate_for_session / drain / release
Autoscaler
reconcile!(metrics) → ensure spare / scale-in idle
Tag VM Cloud: matchlivetv, role=stream-node, env=prod|lab.
10. Lab sul Proxmox attuale
Prima (o in parallelo) al Cloud “vero”:
- Template VM
stream-nodesu Proxmox. CLOUD_PROVIDER=proxmox_lab→ clone/start/stop via API Proxmox.- DNS lab (hosts/dnsmasq).
- Soft cap basso; N sessioni di test; verifica assignment, spare, scale-in, alert disco.
- Poi smoke su Hetzner Cloud (1 CX piccolo) + DNS reale.
Cosa il lab non replica al 100%: tempi boot Hetzner, vSwitch, RTMP 4G multi-nodo (smoke WAN dopo).
11. Fasi di implementazione
| Fase | Cosa | Esito |
|---|---|---|
| L — Lab Proxmox | LocalLab / ProxmoxLab, DNS lab, admin nodi |
implementata sul branch |
| 0 — Multi-nodo ready | Registry + URL RTMP/HLS in API (StreamNode, Streams::NodeRegistry) |
implementata sul branch |
| 1 — Hetzner Cloud + DNS | HetznerCloudProvider + HetznerDnsProvider, mltv-stream.net, cloud-init, WireGuard (§16) |
implementata sul branch (WG ops manuale) |
| 2 — Routing relay | Coda youtube_relay, sticky owner, cap RELAY_MAX_CONCURRENT |
implementata sul branch |
| 3 — Autoscaler | Soglie + warm spare (+1), kill-switch STREAM_AUTOSCALE_ENABLED |
implementata sul branch |
| 4 — Hardening | Drain sicuro, budget, kill-switch Redis/admin, alert overflow, runbook | implementata sul branch (solo test lab — non in prod) |
| A — Auction (futuro) | Migrazione control plane + vSwitch al posto di WireGuard | Hardware dedicato Hetzner |
| B2 (opz.) | Switch object storage | Quando misurato |
Fase 0 — dettagli implementati
- Tabella
stream_nodes+stream_sessions.stream_node_id Streams::NodeRegistry.ensure_home_from_env!/allocate!(least-loaded)Sessions::Createassegna il nodo e crea il path MediaMTX sul client del nodo- URL RTMP/HLS/API/internal per sessione derivati dal nodo (fallback ENV se nodo assente)
- API JSON espone
stream_node(slug) - Dominio ingest pubblico futuro:
*.mltv-stream.net
Fase L — dettagli implementati
Streams::CloudProviders(local_lab,proxmox_lab, stubhetzner)Streams::DnsProviders(labsu Redis, stubhetzner)Streams::NodeProvisioner(provision / drain / decommission)- Admin Nodi stream (
/admin/stream_nodes): lista, provision lab, drain, delete - Rake:
streams:nodes:ensure_home,streams:nodes:provision_lab,streams:nodes:dns_lab_dump - Default sicuro:
STREAM_CLOUD_PROVIDER=local_lab(nessuna chiamata Proxmox finché non configuri token)
Fase 1 — dettagli implementati
Streams::CloudProviders::Hetzner(create/destroy server, wait running, labels)Streams::DnsProviders::Hetzner(upsert/delete A su zonamltv-stream.net)NodeProvisioner#provision_cloud!+ bottone admin Provisiona nodo Hetzner- Cloud-init:
infra/stream-node/cloud-init.yaml(HCLOUD_USER_DATA_FILE) - Decommission usa il provider del nodo (non solo ENV globale)
- WireGuard: vedi §16
Fase 2 — dettagli implementati
- Coda Sidekiq dedicata
youtube_relay(priorità sopradefault) YoutubeRelayEnsureJob/YoutubeRelayStopJobsolo su quella coda- Stop sticky: non cancella owner da Rails; lo stop gira sull’owner o requeue (
:wrong_host) - Cap per worker:
RELAY_MAX_CONCURRENT(default 4) + set Redisyoutube_relay:owned:HOSTNAME - Ensure a capacità piena → requeue 5s invece di avviare un secondo ffmpeg locale
Fase 3 — dettagli implementati
Streams::Autoscaler+ job chainStreams::AutoscalerJob(come health monitor)- Kill-switch:
STREAM_AUTOSCALE_ENABLED=1per attivare - Scale-out se
free_slots ≤ STREAM_AUTOSCALE_SOFT_FREE_SLOTS - Warm spare (+N) se carico/soglia e
spare_ready < WARM_SPARE - Scale-in overflow idle da
IDLE_MINUTES(non tocca home; rispetta warm spare) - Cap
STREAM_AUTOSCALE_MAX_NODES; kindlab|cloud - Metriche in admin Nodi stream
Fase 4 — hardening (implementata sul branch)
NON rilasciare in produzione finché lab + smoke Cloud non sono verdi. Su questo branch restano
STREAM_AUTOSCALE_ENABLED=0eSTREAM_AUTOSCALE_ALLOW_CLOUD=0.
- Scale-in sicuro:
drain!→ attesa sessioni zero →decommission! - Budget soft:
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR+STREAM_AUTOSCALE_NODE_EUR_PER_HOUR(stima 24/7); scale-out bloccato se sforerebbe - Cloud autoscale solo con
STREAM_AUTOSCALE_ALLOW_CLOUD=1eHCLOUD_TOKEN - Kill-switch runtime Redis
streams:autoscaler:kill_switch(bottoni admin ON/OFF) oltre all’ENV - Health check
stream_overflow→ incidente Ops/ntfy su nodi idle orfani, over-budget, capacità al max - Admin: metriche budget + stato kill-switch
Runbook operativo (lab / pre-prod)
| Situazione | Azione |
|---|---|
| Dubbio / picco anomalo | Admin → Kill-switch ON (o STREAM_AUTOSCALE_ENABLED=0) |
| Nodo spillato | Drain dal admin; dopo fine live → Destroy |
| Budget alert Ops | Abbassa MAX_NODES / alza budget solo dopo review costi Hetzner |
| Smoke Cloud | Provision manuale admin (kind cloud), non autoscaler; verifica WG + RTMP 1935 |
| Scale-out lab OK | Solo dopo: considerare ENABLED=1 + KIND=lab su staging/lab Proxmox |
| Produzione overflow | Solo dopo checklist §12 punto 6 |
Ordine di lavoro consigliato sul branch: 0 → L → 1 → 2 → 3 → 4; A quando si decide di lasciare il Proxmox casa.
12. Ordine operativo “ora” (senza Auction)
- Rinforzare dischi/alert sul Proxmox prod (§6).
- Secondo dominio + zona Hetzner DNS.
- Progetto Hetzner Cloud + immagine/snapshot
stream-node. - Tunnel WireGuard Proxmox ↔ Cloud (Redis/DB/API private).
- Implementare fasi 0 → L → 1… sul branch.
- Cutover overflow in produzione solo dopo lab + smoke Cloud verdi.
13. Criteri di successo
| Criterio | Misura |
|---|---|
| Lab | Scale-out/in e assignment verificati su Proxmox senza Hetzner |
| Picco | N live oltre soft cap home con spare Cloud ready |
| Continuità | Drop telefono → slate YT attiva |
| Disco | Nessun outage DB/API per disco video; alert prima del critical |
| DNS | Aruba intatto; ingest sul secondo dominio |
| Futuro | Path chiaro verso Auction senza riscrivere autoscaler |
14. Aperto / da calibrare
| Voce | Note |
|---|---|
| Nome secondo dominio | mltv-stream.net (chiuso) |
Soft cap stream-home |
es. 4 vs 6 |
| Tipo VM Cloud | CPX vs CCX |
OVERFLOW_MAX / budget |
— |
| Spare di notte | 0 vs 1 |
| Dettaglio WireGuard | subnet, peer, MTU |
| Soglie GB disco | da size reale volume video |
15. Riepilogo esecutivo
Ora: restiamo sul Proxmox di produzione; attiviamo Hetzner Cloud (spare MediaMTX+ffmpeg) + Hetzner DNS (secondo dominio) + WireGuard. Testiamo prima in lab sullo stesso Proxmox. Garage resta; B2 dopo. YouTube consigliato in prodotto. Disco e alert sono vincolo non negoziabile.
Dopo: eventuale Auction Hetzner sostituisce il control plane casa e WireGuard → vSwitch, senza cambiare il modello nodi/autoscaler.
16. WireGuard (Proxmox casa ↔ Hetzner Cloud)
Obiettivo: Rails/Sidekiq sull’host di produzione raggiungono api_base_url / internal_rtmp_url dei nodi Cloud (es. http://10.0.0.9:9997) senza esporre Redis/Postgres/MediaMTX API su Internet.
Setup consigliato (una tantum)
- Su Hetzner Console: crea Network privata (es.
10.0.0.0/16) nella locationfsn1, subnet10.0.1.0/24per i server Cloud. - Imposta
HCLOUD_NETWORK_ID=<id>così i nodi stream entrano nella network al create. - Su Proxmox (VM o LXC gateway): installa WireGuard; peer verso una VM “gateway” Cloud sempre on (CX22 piccola) oppure peer site-to-site verso un server Cloud fisso.
- Alternativa più semplice per i primi test: Tailscale su Proxmox host + su ogni stream-node (cloud-init) — stesso effetto, meno ops.
- Firewall Hetzner Cloud:
- WAN:
1935/tcp(RTMP),443/tcpo8888se HLS diretto - Solo rete privata / WG:
9997(MediaMTX API), niente Postgres/Redis sui nodi stream
- WAN:
- Verifica da Rails:
curl http://<private_ip>:9997/v3/paths/list
Finché WireGuard/Tailscale non è pronto, api_base_url punta comunque alla private IP (o pubblica se manca private_net): il path MediaMTX da Create fallirà dal control plane se non raggiungibile — provisionare nodi Cloud solo dopo connettività privata OK, oppure smoke con API su IP pubblico temporaneo + firewall allowlist IP casa.
ENV produzione (stream autoscale)
HCLOUD_TOKEN=...
HCLOUD_LOCATION=nbg1
HCLOUD_SERVER_TYPE=cpx12
HCLOUD_IMAGE=debian-12
HCLOUD_SSH_KEY=matchlivetv-stream-hetzner
HCLOUD_NETWORK_ID= # opzionale
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
STREAM_DNS_ZONE=mltv-stream.net
STREAM_DNS_TTL=60
STREAM_CLOUD_DNS_SUFFIX=mltv-stream.net
STREAM_CLOUD_MAX_PUBLISHERS=4
STREAM_NODE_ENV=prod
# Default lab sicuro; in prod per overflow usa hetzner esplicitamente dal bottone admin
STREAM_CLOUD_PROVIDER=local_lab
STREAM_DNS_PROVIDER=lab