Salta ai contenuti

Come avviare un ambiente di microservizi in locale

Avviare un microservizio in locale è facile: lo dice il suo README. I problemi cominciano quando il flusso che vuoi provare ne attraversa quattro, e ognuno ne chiama altri due che non sono tuoi.

Questa pagina non è il tutorial di uno strumento specifico: è quali pezzi servono, perché la parte difficile non è quella che sembra, e come si risolve ciascuna — con Aseptic alla fine, perché è ciò che questo sito documenta, ma i quattro problemi sono gli stessi qualunque cosa tu usi.

Un ambiente locale che serva a qualcosa deve risolvere quattro cose. In questo ordine di difficoltà reale, che quasi mai è l’ordine con cui le si affronta:

  1. Infrastruttura comune. Kafka, Redis, PostgreSQL, quello che usa il tuo sistema. È la parte più visibile e la più risolta: i container.
  2. Avvio di ogni servizio. Il suo comando, il suo profilo, le sue variabili, la sua porta. Lavoro ripetitivo, ma meccanico.
  3. Comunicazione fra i servizi. Dove chiama ciascuno. È qui che se ne va la mattina, ed è ciò di cui quasi nessuno parla.
  4. Dati. Uno schema creato e qualcosa dentro con cui provare.

Un docker-compose.yml con i pezzi comuni e docker compose up. È ciò che fanno tutti ed è giusto: i container sono esattamente lo strumento adatto per questo.

Due dettagli che mordono più tardi:

  • Le porte collidono. Ogni team dichiara la 5432 e la 6379. Appena hai due progetti aperti, o alzi e abbassi, cominciano i conflitti.
  • L’infrastruttura si condivide fra progetti, i dati no. Riutilizzare lo stesso Postgres per due sistemi diversi finisce in uno schema di cui nessuno sa di chi sia.

Ogni servizio ha il suo modo: ./mvnw quarkus:dev, ./gradlew bootRun -Dspring.profiles.active=local, npm start, un binario. Con profili, variabili e flag diversi.

È questo che di solito finisce scritto in un LOCAL_SETUP.md di quaranta passi: funziona, ma invecchia male. Nessuno aggiorna il documento quando cambia una flag, e chi arriva dopo scopre lo scarto a suon di errori.

Un dettaglio che decide molto più di quanto sembri: non tutti i servizi vogliono girare allo stesso modo. Quello su cui stai lavorando lo vuoi nativo, con hot reload e con il debugger agganciato. Gli altri tre devono solo essere su: in container vanno benissimo, isolati e senza sporcarti la macchina.

Qui c’è il lavoro vero, ed è quello che nessuno strumento di container ti risolve.

Il tuo servizio bff chiama orders. In produzione quell’URL lo dà la service discovery o l’ingress. In locale, qualcuno deve decidere dove punta, e le opzioni non sono equivalenti:

  • Al servizio avviato sulla tua macchina. È quello che vuoi quando stai cambiando entrambi i lati. Ma allora devi avviarlo anche lui, con tutta la sua coda di dipendenze dietro.
  • All’ambiente condiviso di sviluppo. Ti risparmia di avviarlo, in cambio di una connessione alla sua rete, di dati che cambiano sotto i tuoi piedi e del fatto che se quell’ambiente è giù, lo sei anche tu.
  • A un mock. Perfetto quando la risposta di quel servizio ti è indifferente, o quando non riesci nemmeno a raggiungerlo. In cambio di mantenere lo stub.

Quello che costa non è scegliere: è applicare la scelta. Ogni opzione significa cambiare un URL nella configurazione di un repository che non è tuo, ricordarti di non committarlo, e disfarlo quando vuoi provare altro. Moltiplica per servizi e per volte al giorno.

E c’è una trappola: l’URL corretto dipende da dove gira chi chiama. Un servizio nativo raggiunge un container tramite localhost:porta; un container che ne chiama un altro, tramite il nome del servizio; e un container che chiama qualcosa di nativo sulla tua macchina ha bisogno del nome speciale dell’host. La stessa dipendenza, tre URL diversi a seconda di come sta girando ciascun estremo.

Schema creato, migrazioni eseguite e qualche dato minimo. Di solito si risolve con le migrazioni dell’applicazione stessa più uno script di seed.

La regola che conviene non infrangere: l’ambiente locale non tocca database condivisi. Una migrazione lanciata per sbaglio contro l’ambiente di sviluppo comune è un pomeriggio perso per tutto il team.

Approccio Buono per Dove resta corto
docker-compose di tutto Infrastruttura; un sistema piccolo e stabile Costruire l’immagine del tuo servizio a ogni modifica; niente hot reload né debugger comodi; la comunicazione resta configurazione a mano
Script + LOCAL_SETUP.md Partire in fretta Invecchia: nessuno lo aggiorna e fallisce in modo diverso su ogni macchina
Testcontainers Test di integrazione veri L’ambiente nasce e muore con il test; non è un posto dove lavorare
Kubernetes locale (Kind, minikube) Somigliare alla produzione Tanta macchina e tanto ciclo di build per il quotidiano; il ciclo di modifica si allunga
Orchestratore locale Il ciclo quotidiano con più servizi Bisogna descrivere ogni servizio una volta

Nessuno è sciocco: risolvono cose diverse e spesso convivono. Ciò che nessuno dei primi quattro fa è decidere per dipendenza dove chiama ogni servizio e applicarlo senza toccare i repository.

Aseptic è un orchestratore desktop che si occupa dei quattro pezzi, e tratta il terzo come il problema principale che è.

  1. Descrivi ogni servizio una volta. Punti alla cartella del repository e la sua tecnologia, le sue porte, l’infrastruttura di cui ha bisogno e chi chiama vengono rilevati in automatico. Rivedi e basta.
  2. Raggruppi in uno scenario i servizi di un flusso — «Checkout», «Registrazione cliente» — e si avviano insieme, in ordine e aspettando che ciascuno sia sano.
  3. Per ogni dipendenza scegli locale, cloud o mock, e il motore riscrive l’URL all’avvio iniettandolo come configurazione —flag -D o variabili d’ambiente—, con la prospettiva giusta a seconda di dove gira ciascun estremo. I tuoi repository non si toccano.
  4. L’infrastruttura comune (Kafka, Redis, PostgreSQL) la gestisce l’app in Docker, con rimappatura automatica delle porte quando collidono.
  5. Ogni servizio, nativo o in container, a tua scelta: nativo quello su cui stai lavorando, in container quelli di sfondo.

E quello che monti si condivide: uno scenario si esporta in un file autonomo, così chi arriva clona, importa e avvia invece di leggersi il LOCAL_SETUP.md.