Skip to content

Runbook: aggiornare un deploy PM2 di un repo cliente ​

Portare a un nuovo commit di main un deploy che gira sotto PM2 dai sorgenti del repo cliente: ecosystem.config.cjs di una riga, etl serve dalla radice del repo, nessuna build. È una delle due confezioni dell'artefatto, il repo cliente a un commit; l'altra, l'immagine container, sta in Deploy.

Sintesi ​

  • Cosa fa: ferma l'app PM2, porta il repo al nuovo commit di main, installa le dipendenze dal lockfile (che fissa anche la versione del core), ricostruisce la documentazione del cliente, verifica l'ambiente con etl env check, riavvia e controlla che la prima run registri versione di pipeline, release, core e cliente.
  • Owner: il team che mantiene il repo cliente.
  • Quando: dopo il merge su main di ciò che il deploy deve ricevere, in una finestra senza run in corso (§ 1).
  • Durata: pochi minuti di fermo. Nel frattempo i processi restano pending e il worker li rivendica al riavvio: un worker fermo non perde lavoro (worker).
  • Criticità: media. Un riavvio a run in corso la interrompe, e la run non torna in coda da sola (worker § Arresto e deploy).

L'app PM2 è una sola, e la configurazione la porta il pacchetto del core: il repo ha ecosystem.config.cjs, un file gestito di una riga che chiama ecosystem() di @noeva/etl-core/pm2. La UI è l'app desktop, che si collega a etl serve.

AppCosa giraPortaLog in logs/
noeva-etletl serve: server dei grafi, worker e /guida/9010noeva-etl-out.log, noeva-etl-error.log

La configurazione arriva dal .env alla radice del repo, che la CLI carica da sola prima di ogni comando: etl serve sotto PM2 ed etl env check lanciato dal terminale vedono lo stesso ambiente. Sotto PM2 la porta di default è 9010 (Variabili d'ambiente del core). I comandi qui sotto valgono in PowerShell e in Git Bash; in Windows PowerShell 5 curl è un alias di Invoke-WebRequest, quindi si scrive curl.exe.

0. Prima installazione ​

Una volta sola, sul server, con l'utente di servizio che farà girare PM2:

  1. Il repo e le dipendenze, con Node 26 o successivo e pnpm (la versione la dice packageManager nel package.json):

    bash
    git clone <url del repo cliente> && cd <repo>
    git checkout main
    pnpm install --frozen-lockfile
  2. Il .env, alla radice del repo, partendo dal .env.example: le istanze di connettore, la configurazione delle pipeline e le variabili obbligatorie del core (Variabili d'ambiente del core). Poi pnpm exec etl env check finché non dice Nessuna variabile obbligatoria mancante. (§ 4).

  3. La documentazione del cliente, che etl serve mostra su /guida/: pnpm exec etl docs build.

  4. L'avvio: pm2 start ecosystem.config.cjs, le verifiche del § 5, poi pm2 save e l'avvio al boot del sistema (pm2 startup su Linux, un servizio su Windows).

1. Scegliere il momento ​

L'arresto del worker annulla la run in corso invece di aspettarla, e la chiude failed con Run interrotta dall'arresto del worker: in error_message. Rilanciarla è una decisione di chi opera, perché può aver già prodotto effetti esterni (worker § Arresto e deploy).

Sotto PM2 l'arresto è cooperativo anche su Windows, dove i segnali non esistono: ecosystem() dichiara shutdown_with_message: true, e PM2 manda al processo il messaggio shutdown, che la CLI ascolta. Se la run non si chiude entro kill_timeout (10 s), PM2 uccide il processo, e la riga resta running e si recupera a mano (worker § Run fallita a metà).

  1. Guarda la cadenza. Il job schedulato di ogni pipeline decide quando parte la prossima run: il momento giusto è subito dopo la fine di una run.

  2. Verifica che nessun processo sia running per i workspace del deploy:

    sql
    SELECT id, function, status, updated_at
    FROM processes
    WHERE workspace_id = '<workspace-id-uuid>' AND worker_type = 'etl' AND status = 'running';

    Nessuna riga: si può procedere. Una riga: aspetta che finisca. Il log del worker lo dice con meno certezza: ogni Processo rivendicato deve avere un Processo completato (o Processo chiuso dall'arresto del worker) con lo stesso processId.

    bash
    pm2 logs noeva-etl --lines 50 --nostream
  3. Non lanciare run a mano (etl run) durante l'aggiornamento.

Fra il controllo e l'arresto il worker può ancora rivendicare un processo: per questo il § 2 ferma subito, e rifà la query dopo l'arresto.

2. Fermare e annotare da dove si parte ​

Dalla radice del repo:

bash
git rev-parse HEAD
pm2 stop noeva-etl

Annota lo SHA stampato da git rev-parse nel ticket del rilascio: è il punto a cui torna il rollback del deploy (§ 6). Si ferma prima dell'install perché il processo carica alcuni moduli del core solo quando servono, e con node_modules cambiato sotto di sé li prenderebbe da un'altra versione.

Rifai la query del § 1, e cerca anche le run chiuse dall'arresto: il worker può aver rivendicato un processo nell'ultimo momento. Una run annullata è failed con Run interrotta dall'arresto del worker: in error_message; la query per trovarla e rimetterla in coda è in worker § Arresto e deploy. Una riga ancora running vuol dire che il processo è stato ucciso oltre kill_timeout: si recupera con worker § Run fallita a metà. In entrambi i casi verifica prima gli effetti già prodotti, e rimettila in coda dopo il riavvio.

3. Portare il repo al nuovo commit ​

bash
git status --short

Il checkout dev'essere pulito sui file tracciati. Una modifica locale fa fallire il merge più sotto e rende la release -dirty (§ 5): capisci da dove viene prima di toccarla. logs/, .runs/, .state/, .env e il sito costruito in docs/.vitepress/ sono ignorati da git e non contano.

bash
git fetch origin
git checkout main
git merge --ff-only origin/main

Se il repo ha submodule (per esempio uno script che una pipeline lancia), portali al commit che main registra, ogni volta: un submodule rimasto indietro lascia il checkout modificato, e la release esce -dirty.

bash
git submodule update --init

Per un commit preciso al posto della punta di main: git checkout --detach <sha>, seguito dallo stesso git submodule update --init. Il merge --ff-only si rifiuta se il main locale ha commit che origin/main non ha: in un deploy non dovrebbero esserci.

Leggi cosa arriva, fra lo SHA annotato e il nuovo HEAD:

bash
git diff --stat <sha-precedente> HEAD -- package.json pnpm-lock.yaml .nvmrc ecosystem.config.cjs
git log --oneline <sha-precedente>..HEAD -- src/pipelines
  • package.json con un'altra versione di @noeva/etl-core: arriva un core nuovo. Il suo changelog sta nel pacchetto (node_modules/@noeva/etl-core/CHANGELOG.md, dopo il § 4): le voci «Cambia il piano» spostano versioni di pipeline, e il § 4 può trovare variabili nuove del core.
  • .nvmrc cambiato: la versione di Node richiesta è cambiata, verifica node --version prima di proseguire.
  • ecosystem.config.cjs cambiato: va bene così, il § 5 ricarica la configurazione.
  • src/pipelines: quali pipeline cambiano versione. Il changelog di ciascuna è nella sezione Versioni della sua scheda.

4. Dipendenze, documentazione e .env ​

bash
pnpm install --frozen-lockfile
pnpm exec etl docs build

--frozen-lockfile installa esattamente il lockfile del commit, core compreso, come la CI, e si ferma invece di modificarlo: un lockfile modificato renderebbe la release -dirty. Il core è pubblico su npmjs (ADR-0105) e si installa senza token: un 404 su @noeva/etl-core vuol dire che la versione del lockfile non è sul registry, oppure che un .npmrc (del repo o dell'utente) manda lo scope @noeva a un altro registry.

pnpm exec etl docs build ricostruisce il sito del cliente, che etl serve legge all'avvio e non costruisce da sé: saltato, /guida/ resta quello del commit precedente. Si ferma su una scheda, uno scopo o un link mancante: è lo stesso gate di etl ci, quindi su un commit di main non dovrebbe succedere.

Se il repo cliente ha dipendenze fuori da Node (un interprete, uno script in un submodule), i loro passi stanno nel runbook del repo cliente, e vanno fatti qui.

Le variabili nuove. .env non è versionato, quindi un aggiornamento non lo tocca. Quali variabili il codice nuovo vuole lo dice etl env check, sull'intero catalogo e con il .env del deploy, che la CLI carica da sola (Variabili d'ambiente del core):

bash
pnpm exec etl env check
  • Su stdout una riga per ogni variabile obbligatoria mancante, con chi la dichiara: core, pipeline <nome> o istanza <connettore> <id> (<pipeline>). Comprese quelle che aggiunge il core, che il diff di .env.example non mostra. Con almeno una mancante il comando esce con 1, e il § 5 si fermerebbe con un ConfigError.
  • Su stderr un Avviso: per ogni chiave del .env che nessuno dichiara: una variabile rinominata o tolta dal rilascio, o un refuso. Non ferma niente, ma va capita: spesso accanto c'è la mancante col nome nuovo. Una pipeline che non dichiara le sue variabili (environment) le fa conoscere solo dal suo piano, che non le mostra tutte: l'utente di un DB o una lista <TIPO>_INSTANCES che usa possono comparire qui benché servano. Prima di togliere una chiave, controlla nella scheda della pipeline.

Prima di modificare .env, salva una copia accanto, con la data nel nome: serve al rollback. Poi riporta ogni variabile mancante e rilancia il check finché non dice Nessuna variabile obbligatoria mancante.

  • ETL_PIPELINE_WORKSPACES: se è impostata, filtra le pipeline fra i workspace serviti. Una pipeline nuova non va aggiunta, a meno che il deploy non la limiti a certi workspace; un nome che non è più nel catalogo, per esempio una pipeline rinominata, ferma l'avvio (worker § Pipeline per workspace).
  • ETL_RELEASE: sotto PM2 lasciala commentata. Se è impostata vince sul commit, e un valore rimasto da un rilascio precedente fa registrare alle run la release sbagliata.

5. Riavviare e verificare ​

bash
pm2 reload ecosystem.config.cjs
pm2 ls

pm2 reload rilegge ecosystem.config.cjs e riavvia l'app, e il processo rilegge .env all'avvio. L'app deve risultare online, con il contatore dei restart fermo. Se l'avvio al boot passa dalla lista salvata di PM2, aggiornala con pm2 save.

I log di avvio, da pm2 logs noeva-etl --lines 50 --nostream. Stanno in due file, e non è un errore: le righe della CLI vanno su stderr (noeva-etl-error.log), quelle del worker su stdout (noeva-etl-out.log). Devono comparire tutte:

  • in noeva-etl-error.log, Server grafi in ascolto su :9010 (<N> grafi) e Avviati <N> worker (workspace: …), più una riga per workspace con le pipeline ammesse se ETL_PIPELINE_WORKSPACES è impostata;
  • in noeva-etl-out.log, Worker avviato, inizio polling e Catalogo pipeline annunciato, una per workspace.

Un ConfigError al posto di queste righe vuol dire che .env non è completo per il codice nuovo: il messaggio nomina la variabile. PM2 lo riavvia a vuoto finché non lo marca errored: correggi .env, controlla con pnpm exec etl env check e rilancia pm2 restart noeva-etl. Un Avviso: su documentazione del cliente non costruita vuol dire che il § 4 ha saltato pnpm exec etl docs build.

Il deploy servito. GET /deploy dice cliente e versione del core, GET /graphs la versione di pipeline corrente di ogni pipeline, l'ultima voce del suo changelog (server grafi):

bash
curl.exe -s localhost:9010/health
curl.exe -s localhost:9010/deploy
curl.exe -s localhost:9010/graphs

Le pipeline toccate dal rilascio devono avere la versione nuova, e core in /deploy la versione installata (pnpm list @noeva/etl-core). localhost:9010/guida/ mostra la documentazione del cliente, e localhost:9010/guida/core/ la guida del core che arriva col pacchetto.

La prima run. Aspetta la run successiva del job schedulato, o avviala dall'app desktop, poi leggi il suo report (report di run § Versione di pipeline e release):

sql
SELECT id, function, status, completed_at,
       output_data ->> 'reportSchemaVersion' AS schema,
       output_data ->> 'pipelineVersion' AS pipeline_version,
       output_data ->> 'release' AS release,
       output_data ->> 'core' AS core,
       output_data ->> 'client' AS client
FROM processes
WHERE workspace_id = '<workspace-id-uuid>' AND worker_type = 'etl'
ORDER BY completed_at DESC NULLS LAST
LIMIT 5;

Lo stesso report sta nello storico locale del deploy: pnpm exec etl runs list, poi pnpm exec etl runs show <runId> --json, oppure GET /runs sulla porta 9010.

CampoAtteso
reportSchemaVersion8
pipelineVersionil numero che GET /graphs dà per quella pipeline, mai null
releasegit:<sha a 12 caratteri> del nuovo HEAD, senza -dirty
corela versione di @noeva/etl-core installata
clientil client di etl.config.ts

La release attesa si ottiene dalla radice del repo, la cartella dell'etl.config.ts, con lo stesso comando che usa la CLI:

bash
git describe --always --dirty --abbrev=12 --exclude "*"

Se il report non corrisponde:

  • release è lo SHA vecchio: il processo non è stato riavviato dopo il checkout. La release si calcola una volta sola, all'avvio.
  • release finisce in -dirty: il checkout ha modifiche a file tracciati. git status le mostra; la causa più comune è un lockfile riscritto da un install senza --frozen-lockfile.
  • release è un valore che non è un commit: ETL_RELEASE è impostata in .env (§ 4).
  • release è null: la cartella dell'etl.config.ts non è la radice di un checkout, o git non è raggiungibile dall'utente con cui gira PM2. Controlla cwd in pm2 describe noeva-etl.
  • core non è la versione attesa: il § 4 non ha installato, o l'install ha usato un altro lockfile. Ricontrolla pnpm list @noeva/etl-core.
  • pipelineVersion è null o assente, o lo schema è sotto 8: il processo non sta eseguendo il codice nuovo. Ricontrolla git log -1 e il riavvio.

Infine l'app desktop: collegata a http://<server>:9010, deve mostrare il nome del cliente e i grafi.

6. Tornare al rilascio precedente ​

È il rollback del deploy: quando il rilascio intero regredisce e non c'è tempo per una correzione, si ripete la procedura verso lo SHA annotato al § 2. Riporta indietro tutto, core compreso, perché il lockfile ne fissa la versione. Stessa scelta del momento (§ 1), poi:

bash
pm2 stop noeva-etl
git checkout --detach <sha-precedente>
pnpm install --frozen-lockfile
pnpm exec etl docs build

Ripristina la copia di .env salvata al § 4: con il codice vecchio una variabile nuova è inutile, ma in ETL_PIPELINE_WORKSPACES una pipeline che il codice vecchio non conosce ferma l'avvio. Poi pm2 reload ecosystem.config.cjs e le verifiche del § 5, che ora si aspettano la release git:<sha-precedente>, il core e le versioni di pipeline di prima.

Il rollback del deploy riporta indietro il codice, non i dati:

  • i dati già scritti dal rilascio ritirato restano dove sono;
  • lo stato di pipeline non torna indietro. Se il rilascio ritirato ha avanzato un watermark, o ha cambiato cosa misura, valuta un reset con stato incrementale § Reset;
  • il deploy ora è dietro main. Il prossimo aggiornamento riporta il rilascio ritirato, quindi la correzione va su main prima di riaggiornare.

Una pipeline sola, o il core ​

Se a regredire è una pipeline sola, non serve riportare indietro tutto il deploy: il rollback di pipeline (pnpm exec etl versions rollback <pipeline> <N> --motivo "…" --ticket <chiave>) prepara un hotfix/* da main che riporta solo la cartella della pipeline a una sua versione precedente, come versione nuova. Se a regredire è il core, il downgrade del core è una PR hotfix/* che abbassa @noeva/etl-core nel package.json. Le procedure sono in Rollback di una pipeline; dopo il merge su main, l'hotfix arriva al deploy con questo stesso runbook.

Escalation ​

  • Primo livello: chi mantiene il repo cliente, con lo SHA di partenza e d'arrivo, l'output di pm2 ls e di pnpm exec etl env check, e le ultime righe di logs/noeva-etl-error.log.
  • Un problema che viene dal core (un ConfigError del core dopo un bump, una regressione dopo «Cambia il piano»): il team del core, con la versione di partenza e d'arrivo di @noeva/etl-core.
  • Una run interrotta dall'aggiornamento con effetti esterni dubbi: l'owner della pipeline, dalla sua scheda in docs/pipelines/, prima di rimetterla in coda.

Noeva è un marchio registrato di 4D S.R.L.