Tema
Runbook: distribuzione del runner ETL
Quello che si mette in servizio è l'artefatto: il repo cliente a un commit, con il suo lockfile, che fissa anche la versione del core. Ha due confezioni, entrambe avviate con etl serve dalla radice del repo: l'immagine container di questo runbook, costruita dalla CI del repo cliente e pubblicata su GHCR, e PM2 dai sorgenti, in Aggiornare un deploy PM2. Questo runbook copre anche la modalità batch (etl run, non un daemon: un container = una esecuzione di pipeline, poi termina).
Il CMD di default dell'immagine però è serve: server HTTP dei grafi e worker nello stesso processo, un solo container per tutta la piattaforma (runbook graph-server.md). Il worker da solo (etl worker, poller sulla tabella processes di Noeva, anche multi-workspace) è in etl-worker.md: mai un container worker in parallelo a un serve, o i job vengono presi due volte.
Configurazione solo da variabili d'ambiente (nessun segreto nell'immagine).
La UI non si deploya da qui.
noeva-etl-uiè un'app desktop Tauri, che si collega all'URL dietl serve: non fa parte dell'artefatto.
Modello d'uso attuale: l'artefatto è l'immagine del repo cliente (sotto) o PM2 dai sorgenti. Un cliente può scegliere ECR + ECS invece di GHCR, con un servizio ECS suo (§ Un cliente su ECR + ECS); chi non è ancora migrato al suo repo si deploya così dal repo del core.
CLI
bash
etl list # elenca le pipeline disponibili
etl run <pipeline> # esegue una pipeline
etl serve # default dell'immagine: grafi via HTTP + worker
etl worker # solo i worker
etl deploy-ecr <cliente> # l'immagine di un cliente su ECR + ECS (sotto)Prima di ogni comando la CLI carica da sola il .env alla radice del repo, se c'è, e le variabili già nel processo vincono: etl run da un terminale vede lo stesso ambiente di etl serve sotto PM2. Nell'immagine il .env non c'è (lo esclude il contesto della build), e l'ambiente arriva da docker run --env-file o dall'orchestratore.
Immagine del repo cliente
Il Dockerfile non sta nel repo cliente: lo porta il pacchetto del core, @noeva/etl-core/deploy/Dockerfile, con il suo Dockerfile.dockerignore accanto, e arriva aggiornato con il bump. L'immagine parte da node:26-slim e installa il repo con pnpm install --prod --frozen-lockfile: il core arriva dal lockfile come ogni altra dipendenza, e non c'è un'immagine base del core. Non c'è build del codice, che gira dai sorgenti con tsx; si costruisce invece la documentazione del cliente (etl docs build), che etl serve mostra su /guida/, mentre la guida del core su /guida/core/ è già costruita nel pacchetto installato. ENTRYPOINT è etl e CMD è serve, quindi … run <pipeline> lancia una run sola.
La costruisce image.yml, il file gestito che etl init scrive nel repo cliente, lanciato a mano (Actions → Immagine → Run workflow) o spingendo un tag v*, mai a ogni push:
- pubblica su
ghcr.io/<owner>/noeva-etl-<slug>, il GHCR dell'owner del repo, conGITHUB_TOKEN, che dopo un transfer segue il repo; - il tag è lo SHA del commit, sempre, più un tag facoltativo: quello dato a mano o il tag git spinto. Niente
latestné timestamp: il workflow rifiutalatest. Il deploy cita lo SHA, e il rollback del deploy è «torna allo SHA precedente»; - il core si installa da npmjs senza credenziali (ADR-0105): nessun segreto entra nella build;
ETL_RELEASEarriva come build arg,git:<sha a 12 caratteri>, ed è la release che il report di ogni run registra, accanto acoreeclient.
In locale, dalla radice del repo cliente e dopo pnpm install (il Dockerfile sta in node_modules):
bash
docker build \
-f node_modules/@noeva/etl-core/deploy/Dockerfile \
--build-arg ETL_RELEASE=git:$(git rev-parse --short=12 HEAD) \
-t noeva-etl-<slug>:$(git rev-parse HEAD) .
docker run --rm --env-file .env -p 3005:3005 noeva-etl-<slug>:<sha> # serve
docker run --rm --env-file .env noeva-etl-<slug>:<sha> run <pipeline> # una runIl container gira come utente node, e .runs/ e .state/ dentro /app sono suoi: lo storico delle run e lo stato di pipeline locale si perdono con il container, a meno di montare un volume. Un cliente che ha bisogno di altro nell'immagine (un CA aziendale, un proxy, un interprete) copia il Dockerfile nel suo repo, dichiara image.yml in unmanaged se lo modifica, e se ne assume l'aggiornamento.
Un cliente su ECR + ECS
Un cliente può scegliere ECR invece di GHCR, con un servizio ECS suo e il .env in un bucket S3 (ADR-0094 §1, ADR-0104). Lo strumento arriva col pacchetto: dalla radice del repo cliente, che porta la sua deploy-ecr.conf,
bash
pnpm exec etl deploy-ecr <cliente> --dry-run # solo i controlli
pnpm exec etl deploy-ecr <cliente> --attendi # gate, build, push, CodePipelineL'immagine è la stessa di image.yml (o il Dockerfile del repo, se l'ha preso), con due tag su ECR: quello di deploy e quello di rilascio. La procedura completa, la deploy-ecr.conf e la creazione del servizio in noeva-terraform sono in un cliente su ECR + ECS.
Esecuzione
bash
# una tantum: la tabella di destinazione deve esistere (DDL versionata nel repo cliente)
docker run --rm <image> list
docker run --rm --env-file .env <image> run <pipeline>Pipeline incrementali: senza
--workspacelo stato di pipeline è un file inETL_STATE_DIR(default.state/), e il filesystem di un container--rmsi perde a ogni run: la pipeline ripartirebbe sempre dainitial. Usarerun <pipeline> --workspace <id>(stato sunoeva-server-api, lo stesso del worker), oppure montare un volume scrivibile dall'utente del container suETL_STATE_DIR. Vedi stato incrementale.
Variabili richieste: quelle della pipeline, elencate nella sua scheda e in .env.example, più quelle del core (Variabili d'ambiente del core). In produzione iniettarle da un secret manager (AWS Secrets Manager / SSM), non da .env.
Modelli di deploy per cliente
Il servizio gira tipicamente sull'infra del cliente. L'immagine abilita i primi tre modelli; PM2 gira dai sorgenti, senza immagine:
| Modello | Come | Note |
|---|---|---|
| Gestito da noi | Un servizio ECS per cliente con etl serve, definito in Terraform | Il job parte dallo scheduler di Noeva |
| ECS del cliente | Stessa immagine, task definition nel loro account | Solo accesso al registry + secrets del cliente |
| Docker locale del cliente | docker run schedulato da cron sul loro host | Più semplice; idempotenza rende sicuri retry/sovrapposizioni |
| PM2 dai sorgenti | ecosystem() del pacchetto: etl serve dal repo, senza build | Per un server Windows senza Docker |
Scheduling
La pipeline è idempotente (upsert/MERGE su chiave naturale): rieseguire o sovrapporre run non duplica dati. Quindi lo scheduler può essere semplice:
- cron che lancia
docker run ... run <pipeline>; - ECS Scheduled Task (cron via EventBridge);
- per dipendenze/backfill complessi in futuro → orchestratore (es. Temporal TS).
Networking
Il container deve raggiungere sorgente e sink (PG, SQL Server o MySQL), che possono stare su host/VPC diversi. In locale, dai container verso DB sull'host: usare host.docker.internal come *HOST. In cloud: curare security group / routing verso entrambi i DB.
Osservabilità — il report di run è il risultato
Il servizio è stateless: emette un report e termina (niente DB lato servizio).
stdout= il report in JSON (un oggetto).stderr= log umani.- exit code:
0sesuccess/partial,≠0sefailed(utile per alerting dello scheduler). - Anche in errore viene emesso un report
status: "failed".
bash
# cattura il report e i log separatamente
docker run --rm --env-file .env <image> run <pipeline> > report.json 2> run.log
# archivialo dove vuoi (è il chiamante a deciderlo): file, S3, CloudWatch, ...Schema completo e semantica: docs/conventions/run-report.md.

