Microservizi
Tutti i microservizi che compongono la piattaforma devo rispettare i seguenti standard e regole generali
Tutti i servizi devono rispettare i 12factor, descritti in modo anche più dettagliato nell'articolo An illustrated guide to 12 Factor Apps
Packaging
Obiettivo
Ogni microservizio deve poter essere eseguito facilmente in locale, anche da chi non conosce a fondo il progetto. L’obiettivo del packaging è:
facilitare l’avvio dell’applicativo;
semplificare lo sviluppo e il debugging;
rendere accessibile un ambiente funzionante con un semplice
docker-compose up
.
Struttura del repository
Nella root del repository devono essere presenti i seguenti file:
Dockerfile
Build dell’immagine base del servizio
docker-compose.yml
Definisce i container minimi per l’esecuzione in produzione o ambienti CI
docker-compose.dev.yml
Contiene configurazioni utili per lo sviluppo locale (es. build
, mount volumi, strumenti debug)
.dockerignore
(opzionale) Esclude file e cartelle non rilevanti dalla build
Principi e buone pratiche
Il file
docker-compose.yml
deve essere autosufficiente: chi lo scarica deve poter eseguire il servizio senza dover clonare l’intero repository.Le immagini Docker utilizzate devono essere:
disponibili su un registry pubblico (es. DockerHub, GitHub Container Registry ecc.);
oppure facilmente costruibili in locale tramite override.
⚠️ Non includere la chiave
build:
neldocker-compose.yml
principale, per evitare conflitti nei contesti di esecuzione remota (es. CI/CD).
Sviluppo locale
Per lo sviluppo in locale:
Copia o rinomina il file
docker-compose.dev.yml
indocker-compose.override.yml
Esegui:
docker-compose up
Docker gestirà automaticamente il merge tra
docker-compose.yml
edocker-compose.override.yml
(vedi: Docker Docs – Multiple Compose Files).
✅ In questo modo, chiunque può avviare l’ambiente di sviluppo senza modificare i file originali.
Esempio di uso dei file
docker-compose.yml
(semplificato)
version: "3.9"
services:
myservice:
image: registry.gitlab.com/opencity-labs/myservice:1.2.3
ports:
- "8080:8080"
docker-compose.dev.yml
version: "3.9"
services:
myservice:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./:/app
environment:
- ENVIRONMENT=local
Dopo aver rinominato docker-compose.dev.yml
in docker-compose.override.yml
, il comando:
docker-compose up
...utilizzerà in automatico l’immagine build
locale con i volumi montati per lo sviluppo.
Terminazione Pulita
Obiettivo
Ogni microservizio deve essere in grado di gestire correttamente la terminazione, in modo da:
garantire la consistenza dei dati;
evitare la perdita di eventi o operazioni incomplete;
chiudere le risorse in uso in maniera sicura (DB, Kafka, file, socket...).
La gestione corretta della terminazione è essenziale sia in ambiente Docker che in Kubernetes, dove il segnale di terminazione (SIGTERM
) viene inviato prima di un restart o di un downscaling.
Segnali da gestire
SIGTERM
Richiesta di terminazione gentile
Il servizio avvia lo shutdown controllato
SIGKILL
Terminazione forzata e immediata
Non gestibile dal codice; da evitare
✅ Il servizio DEVE catturare
SIGTERM
e avviare una sequenza di terminazione controllata.
Comportamento atteso
Alla ricezione di SIGTERM
, il servizio deve:
smettere di accettare nuove richieste o eventi;
completare le operazioni critiche in corso;
liberare risorse (connessioni, file, lock...);
loggare la terminazione a livello
INFO
;uscire con exit code 0 entro un tempo ragionevole.
Checklist – Terminazione Pulita
Cattura SIGTERM
Il servizio intercetta correttamente il segnale SIGTERM
☐
Terminazione controllata
Avvia una sequenza di chiusura ordinata
☐
Timeout ragionevole
Si chiude in max 10s (o valore configurato)
☐
Operazioni critiche completate
Nessuna perdita o corruzione dati
☐
Chiusura delle risorse
Socket, consumer, file, connessioni DB...
☐
Log di terminazione
Esempio: "SIGTERM ricevuto, shutdown in corso..."
☐
Nessuna nuova richiesta
I listener vengono disattivati
☐
Exit code 0
Il processo si chiude correttamente
☐
Esempio Python – uvicorn + asyncio
uvicorn + asyncio
import asyncio
import signal
import uvicorn
from myservice import app
APP_HOST = "0.0.0.0"
APP_PORT = 8080
def run_server():
"""Esegue il server intercettando SIGINT e SIGTERM per uno shutdown pulito"""
config = uvicorn.Config(
app,
host=APP_HOST,
port=int(APP_PORT),
proxy_headers=True,
forwarded_allow_ips='*',
access_log=False,
)
server = uvicorn.Server(config)
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, lambda: asyncio.create_task(server.shutdown()))
try:
loop.run_until_complete(server.serve())
except asyncio.CancelledError:
pass # Evita traceback in console
if __name__ == '__main__':
run_server()
Esempio Go – signal.NotifyContext
signal.NotifyContext
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Println("Server in avvio su :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Errore durante l'avvio del server: %v", err)
}
}()
<-ctx.Done()
log.Println("SIGTERM ricevuto, avvio terminazione...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Errore durante la terminazione: %v", err)
}
log.Println("Terminazione completata.")
}
Script di test automatico
Usare questo script per validare la terminazione pulita in locale o integrarlo nella CI.
#!/bin/bash
set -euo pipefail
SERVICE_NAME="test_shutdown_service"
IMAGE_NAME="your-image-name"
LOG_FILE="shutdown_test.log"
echo "▶️ Avvio container..."
docker run --rm --name "$SERVICE_NAME" -d "$IMAGE_NAME" > /dev/null
sleep 2 # attesa minima per stabilità
echo "🛑 Invio SIGTERM..."
docker kill --signal=SIGTERM "$SERVICE_NAME"
sleep 2
echo "🔍 Verifica terminazione..."
if docker ps -a --format '{{.Names}}' | grep -q "$SERVICE_NAME"; then
echo "❌ Il container è ancora in esecuzione"
exit 1
else
echo "✅ Terminazione completata correttamente"
fi
Se si usa
docker-compose
, si può integrare il test nel CI lanciando:docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d docker-compose kill -s SIGTERM
Configurazione
Principi
La configurazione dei microservizi deve avvenire:
principalmente tramite variabili d’ambiente;
opzionalmente tramite:
file
.env
(per ambienti locali);parametri CLI (per job schedulati o debug interattivo).
Best practice
Variabili d’ambiente
Default per tutti i deployment (prod/dev/test)
.env
file
Sviluppo locale o test manuali
CLI parametri
Script cron, job schedulati, test/debug
❗ Le configurazioni non devono essere hardcoded nel codice. Utilizzare sempre default sicuri e sovrascrivibili.
Healthcheck
Obiettivo
Permettere al sistema di orchestrazione (Docker/Kubernetes) di verificare che il servizio sia vivo e funzionante.
HTTP microservizi
Deve esporre un endpoint
/status
:200 OK
se tutto è funzionante;codice diverso in caso di errore.
Altri microservizi
Se il servizio non è HTTP, l’healthcheck può essere:
la presenza di un file di stato;
la verifica di un processo in esecuzione.
✅ L’healthcheck DEVE essere incluso nel
Dockerfile
.
Logging
Principi generali
Tutti i microservizi devono implementare un sistema di log coerente, strutturato e facilmente aggregabile.
Riferimento normativo: Allegato 4 delle Linee Guida di Interoperabilità AgID.
I log devono supportare almeno i livelli:
DEBUG
,INFO
,ERROR
. Una gestione completa prevede sei livelli (vedi sotto).Ogni log di errore o anomalia deve essere su una singola riga, facilmente parsabile (plaintext
key=value
o JSON).Evitare log verbosi e non strutturati: ostacolano il monitoraggio e la correlazione cross-microservizio.
⚠️ Non mischiare log di tipo HTTP e stacktrace multilinea.
Scrivere i log a singola riga su
stdout
.Scrivere stacktrace (se necessario) separatamente su
stderr
.
Formato e contenuto dei log
Preferibile l’uso di log in formato JSON puro, ma solo se interamente strutturato.
È vietato:
Mischiare testo libero e JSON nello stesso log.
Includere payload JSON grezzi come stringa in un campo JSON (es. loggare interamente l’evento Kafka).
Campi richiesti in ogni log rilevante
level
✅
Definisce il tipo di log (INFO, ERROR, DEBUG, CRITICAL, WARNING, ecc.)
environment
✅
Definisce l'ambiente in cui il microservizio è in esecuzione
event_id
⚠️
Identificatore univoco dell’evento (se applicabile)
event_type
✅
Tipo di evento o operazione ricevuta
call_type
⚠️
Tipo chiamata: HTTP
, CLI
, EVENT
topic
⚠️
Kafka topic da cui è stato letto l’evento (se applicabile)
log_message
✅
Messaggio del log (errore o info)
client_ip
⚠️
IP della chiamata in ingresso. Usare X-Forwarded-For
quando presente
tenant
✅
identificativo univoco dell'ente a partire dal quale è stato generato il log
provider
, application
, service
✅
Contesto operativo (se disponibili)
user_id
❌
Se presente, deve essere un riferimento anonimo (es. ID utente). Mai dati personali in chiaro
All’avvio, ogni servizio DEVE loggare la versione in esecuzione e l’ambiente (es. local
, boat-qa
, boat-prod
).
Livelli di log
CRITICAL
Fallimento grave, con perdita di dati o che comporta lo l'uscita dal flusso di esecuzione (shutdown) del servizio per impossibilità a proseguire o perché è più safe non proseguire.
No
ERROR
Anomalia che non è possibile gestire e che avrà effetti sul risultato. A ERROR, il rate dei log che si alza dovrebbe essere indice che ci sono problemi per i quali è importante attrarre l'attenzione degli amministratori. Attenzione a non confondere errori che si verificano su sistemi esterni e che non sono nostri errori. Date sempre per scontato che una riga di errore prodotta dovrebbe sempre essere letta da qualcuno, altrimenti meglio esporre un livello di warning o non loggare proprio. Una anomalia dovrebbe sempre dare origine a una e una sola riga di log.
Si
WARNING
Anomalia che non avrebbe dobuto presentarsi ma che è stata gestita correttamente per non comportare errori nel nostro servizio. Ad esempio ho ricevuto un evento con una versione negativa, ma l'ho ignorato. Oppure non sono riuscito a collegarmi al db la prima volta, ma solo dopo 2 tentativi.
Si
INFO
Comunica un cambiamento di stato del servizio. Un evento significativo produce una riga di log per ogni evento o chiamata ricevuta dall'applicativo. Se una transazione o un evento gestito presenta errori NON si deve produrre la riga di INFO e la riga di ERROR, ma solo la seconda.
Si
DEBUG
Questo è il livello in cui comunicare informazioni diagnostiche, non necessarie se non si sta investigando un errore specifico. Devono essere informazioni utili a comprendere errori, non semplicemente a comprendere il flusso interno del software, per il quale esiste il livello apposito (TRACE). Una informazione utile è dare tutto il contesto che permette di comprendere perché il servizio si sta comportando in un certo modo.
No
TRACE
Questo livello è inteso per tracciare il flusso interno del servizio, in modo molto dettagliato. Per esempio si può inserire un log a questo livello per capire in quale ramo del codice siamo finiti con l'esecuzione, oppure si può inserire una riga all'inizio e una alla fine di certe porzioni di codice rilevanti.
No
✅ Ogni anomalia deve generare una e una sola riga di log a INFO, i dettagli vanno a DEBUG.
Esempi di log ben formattati
JSON strutturato
{
"level": "ERROR",
"timestamp": "2025-04-18T10:27:52Z",
"environment": "boat-prod",
"event_id": "abcd-1234",
"event_type": "PaymentRequestReceived",
"topic": "pagopa-incoming-events",
"version": "1.3.2",
"environment": "boat-prod",
"tenant": "9bee12a4-...",
"application": "50b4f598-...",
"service": "0a28427f-...",
"provider": "sicraweb-wsprotocollodm",
"client_ip": "10.12.3.1",
"user_id": "user_92348",
"status": "error",
"message": "Impossibile trovare dati di logon validi per l'utente"
}
Plaintext key=value
ERROR 2025-04-18T10:27:52Z event_id=abcd-1234 tracker=252603771125040 provider=sicraweb-wsprotocollodm tenant=9bee12a4 application=50b4f598 service=0a28427f topic=pagopa-incoming-events microservice=payment-handler version=1.3.2 environment=boat-prod client_ip=10.12.3.1 user_id=user_92348 error="Impossibile trovare dati di logon validi per l'utente; nested exception: Connection aborted (Connection reset by peer)"
Checklist di qualità per log ERROR
ERROR
Usare questa checklist in review e PR:
Monitoring errori
Obiettivo
Raccogliere centralmente gli errori per individuare rapidamente problemi in produzione.
Requisiti
Ogni microservizio DEVE integrare Sentry.
Una volta configurato:
abilitare l’integrazione con Sentry;
definire regole di alert personalizzate, se quelle standard non bastano.
📬 Usare
SENTRY_DSN
come variabile d’ambiente per collegare il servizio a Sentry.
Monitoring metriche
Obiettivo
Esportare metriche di stato e performance per il monitoraggio continuo.
Endpoint Prometheus
Se HTTP, il microservizio DEVE esporre un endpoint
/metrics
in formato Prometheus.
Cosa monitorare
Error counters
✅
Numero errori critici/intermittenti
Response histograms
✅
Tempi di risposta di servizi esterni
Status code metrics
⚠️
Solo se utili (es. 500 frequenti) – altrimenti usiamo Traefik
404 o bot traffic
❌
NON deve essere esposto come metrica – produce rumore inutile
Cron
Principio
Non reinventare un sistema di schedulazione dentro l’applicazione. Usare invece l’orchestratore.
Linee guida
NON implementare schedulatori interni (es. cron manuale, loop time-based).
Il servizio deve essere idempotente: rieseguibile sugli stessi dati senza effetti collaterali.
Esecuzione corretta
Implementare task cron come comandi CLI, eseguibili con la stessa immagine Docker:
docker run your-image-name python cron_jobs/protocol_batch.py
Il job verrà poi schedulato tramite clustered cron (es. crazy-max/cronjob
).
Parametri e file di configurazione
Principio
Niente overengineering: Docker impone già un mapping tra esterno e interno.
Best practice
Path interni
Usa path fissi tipo /data
, senza preoccuparsi del mapping esterno
Configs
Usare Docker Swarm Configs per file di configurazione
Secrets
Usare Docker Swarm Secrets per credenziali e dati sensibili
NO volumes
Evitare l’uso di volume mapping per la configurazione
✅ Questo approccio garantisce sicurezza, semplicità e prevedibilità.
Continuous Integration
Obiettivo
Standardizzare la CI tra i progetti, garantendo efficienza, velocità e qualità.
Requisiti
Ogni progetto DEVE includere la CI condivisa:
stages:
- build
- test
- deploy
- review
- dast
- staging
- canary
- production
- incremental rollout 10%
- incremental rollout 25%
- incremental rollout 50%
- incremental rollout 100%
- performance
- cleanup
include:
- project: 'opencity-labs/product'
ref: main
file: '.gitlab/ci/devops.yml'
Eventuali step personalizzati (es. test, lint, coverage) vanno inseriti dopo lo
include
.Devono riutilizzare la build già fatta tramite artifact, evitando rebuild inutili.
Performance CI
Durata massima della CI: 15 minuti
Durata consigliata: ≤ 5 minuti
⚡ Ottimizzare usando:
build multi-stage Docker;
test selettivi.
Test publiccode.yml
publiccode.yml
Per progetti pubblicati su Developers Italia:
publiccode:
stage: test
allow_failure: false
image:
name: italia/publiccode-parser-go
entrypoint: [""]
script:
- publiccode-parser /dev/stdin < publiccode.yml
only:
- master
Last updated
Was this helpful?