Skip to content

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 di etl 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, con GITHUB_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 latest né timestamp: il workflow rifiuta latest. 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_RELEASE arriva come build arg, git:<sha a 12 caratteri>, ed è la release che il report di ogni run registra, accanto a core e client.

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 run

Il 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, CodePipeline

L'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 --workspace lo stato di pipeline è un file in ETL_STATE_DIR (default .state/), e il filesystem di un container --rm si perde a ogni run: la pipeline ripartirebbe sempre da initial. Usare run <pipeline> --workspace <id> (stato su noeva-server-api, lo stesso del worker), oppure montare un volume scrivibile dall'utente del container su ETL_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:

ModelloComeNote
Gestito da noiUn servizio ECS per cliente con etl serve, definito in TerraformIl job parte dallo scheduler di Noeva
ECS del clienteStessa immagine, task definition nel loro accountSolo accesso al registry + secrets del cliente
Docker locale del clientedocker run schedulato da cron sul loro hostPiù semplice; idempotenza rende sicuri retry/sovrapposizioni
PM2 dai sorgentiecosystem() del pacchetto: etl serve dal repo, senza buildPer 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: 0 se success/partial, ≠0 se failed (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.

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