Health Check non riuscito, ma l'app funziona
Quando un health check non va a buon fine durante il deployment, mentre l'app funziona localmente, di solito le cause sono cinque. Verifica binding, percorso, porta, timing e dipendenze nell'ordine che consente di trovare il problema più rapidamente.
C'è una particolare forma di blocco in cui un deployment bloccato da un health check non riuscito impedisce ogni release, mentre l'applicazione è, sotto ogni aspetto verificabile, perfettamente funzionante. Funziona localmente. Funziona in Docker localmente. I log mostrano che è in ascolto. E la piattaforma segnala un errore dopo l'altro, a volte anche venti di fila, senza che compaia mai una richiesta nei log di accesso.
Quest'ultimo dettaglio è importante e restringe subito il campo. Se l'applicazione non ha mai registrato la richiesta, il check non ha mai raggiunto l'applicazione: quindi non c'è nulla nel codice dell'applicazione che possa spiegarlo.
Ecco le cinque cause, nell'ordine che consente di trovare il problema più rapidamente.
1. Sei in ascolto su localhost
Questa è di gran lunga la causa più comune e spiega esattamente il sintomo "non arriva mai traffico".
All'interno di un container, 127.0.0.1 indica il loopback del container stesso. Un health check proveniente dall'esterno del container non può raggiungerlo. Il processo è in ascolto, i log lo confermano, ma il socket non è raggiungibile da nessun punto che conti davvero.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
I framework hanno valori predefiniti diversi e alcuni li hanno modificati tra una major version e l'altra. Verifica su quale indirizzo esegue effettivamente il bind il tuo framework, invece di basarti su ciò che ricordi.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Se l'indirizzo in ascolto è 127.0.0.1:3000 anziché 0.0.0.0:3000, hai trovato il problema e tutto il resto di questo elenco diventa irrilevante.
2. La porta verificata dalla piattaforma non è quella su cui servi l'app
Sono coinvolte due porte, che è facile confondere: la porta su cui il processo è in ascolto all'interno del container e la porta a cui la piattaforma instrada il traffico. Se l'app legge PORT dall'ambiente e hai impostato 3000 direttamente in un Dockerfile, i due valori possono non coincidere senza che sia evidente.
Il pattern più affidabile consiste nel lasciare che sia la piattaforma a comunicartelo:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Imposta poi la porta del servizio una sola volta, nella piattaforma, e smetti di gestire lo stesso numero in due punti diversi.
3. Il percorso restituisce un valore diverso da un successo
Un percorso di health check viene confrontato esattamente e un numero sorprendente di errori è causato da un redirect. Se l'app reindirizza /healthz a /healthz/ o forza HTTPS con un 301, un checker che considera validi solo gli status 2xx fallirà ogni volta, mentre un browser seguirà il redirect e ti mostrerà una pagina funzionante.
Tre trappole specifiche:
- Redirect dello slash finale.
/healthz→/healthz/è un 301. - HTTPS obbligatorio. Il check interno di solito arriva tramite HTTP semplice sul loopback. Un redirect HTTPS incondizionato lo fa fallire.
- Middleware di autenticazione. Un controllo di autenticazione globale eseguito prima del routing restituirà 401 anche per il percorso di health check.
Escludi esplicitamente il percorso di health check dall'autenticazione e dall'imposizione di HTTPS. È l'unica route che dovrebbe essere assolutamente noiosa.
4. Il check è più rapido del tuo cold start
Se il check fallisce alcune volte e poi passa, oppure fallisce durante il deploy e passa quando riprovi, si tratta di un problema di timing, non di configurazione.
Il budget che ti serve non è quello di un singolo tentativo: è intervallo × retry. Un'applicazione che impiega dodici secondi per connettersi al database e riscaldare una cache ha bisogno di un budget totale superiore a dodici secondi; altrimenti ogni release fallirà e finirai per disattivare il gate, eliminando l'unica cosa che impedisce a una build non funzionante di raggiungere gli utenti.
dockup info my-project/my-api --json | grep -A6 healthCheck
Imposta il timeout al di sopra della durata del singolo tentativo legittimo più lento e configura un numero di retry tale che intervallo × retry superi comodamente il boot legittimo più lento. Misura il boot invece di fare supposizioni: i log contengono i timestamp.
5. L'applicazione non è effettivamente pronta
L'ultimo caso è quello per cui esiste il check: l'app è stata avviata, non è riuscita a raggiungere una dipendenza e sta effettuando dei retry. Non è andata in crash, quindi nulla la riavvia. Non può servire le richieste, quindi il check fallisce. Il sistema funziona esattamente come progettato e ti sta dicendo che questa release non dovrebbe ricevere traffico.
Per distinguere questo caso dagli altri quattro, verifica se l'applicazione ha registrato la richiesta e ha risposto con uno status non 2xx. Se la richiesta compare nei log, le cause da 1 a 3 sono escluse.
L'ordine di diagnostica che fa risparmiare tempo
# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow
# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api
# 4. What is the gate configured to expect?
dockup info my-project/my-api --json
Il passaggio 3 risolve la maggior parte di questi casi. Un curl eseguito dall'interno del container elimina in una volta sola tutte le variabili di rete: se lì restituisce 200 e la piattaforma continua a fallire, il problema riguarda l'indirizzo o la porta, non l'app. Se restituisce un 301 o un 401, hai trovato la causa senza dover intervenire sulla piattaforma.
Perché vale la pena mantenere il gate
Dopo il quarto deploy fallito, è allettante disattivare l'health check e portare a termine la release. Vale la pena ricordare che cosa stai disattivando.
Su Dockup, il health gate è ciò che tiene lontana dagli utenti una release non funzionante. La nuova versione viene compilata e avviata mentre quella attuale continua a servire traffico; il traffico passa alla nuova versione solo quando questa risponde. Disattiva il gate e riattivi lo scenario in cui un container che si avvia ma non può funzionare sostituisce uno che funzionava correttamente.
Un check che fallisce per quattro release consecutive è fastidioso. Un check che passa incondizionatamente è un check che non fermerà il deploy davvero importante.
Domande frequenti
Perché l'health check fallisce quando l'app funziona localmente?
Quasi sempre perché il container esegue il bind su 127.0.0.1 invece che su 0.0.0.0. Localmente ti connetti tramite lo stesso loopback; dall'esterno del container quell'indirizzo non è raggiungibile.
L'endpoint di health deve richiedere l'autenticazione? No. Escludilo dal middleware di autenticazione globale, altrimenti il checker riceverà un 401 e il deploy fallirà anche se l'app funziona.
Quale timeout dovrei usare? Un valore superiore alla durata del singolo tentativo legittimo più lento, con un numero di retry che copra il cold start legittimo più lento. Ricava il tempo di boot dai log invece di fare supposizioni.
È sicuro disattivare l'health check per sbloccare una release? Sblocca la release, ma rimuove la protezione che impedisce a una versione non funzionante di ricevere traffico. Correggi invece il check: nella maggior parte dei casi la causa è un indirizzo di bind o un redirect, e bastano pochi minuti.
