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:

File
Descrizione

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: nel docker-compose.yml principale, per evitare conflitti nei contesti di esecuzione remota (es. CI/CD).

Sviluppo locale

Per lo sviluppo in locale:

  1. Copia o rinomina il file docker-compose.dev.yml in docker-compose.override.yml

  2. Esegui:

    docker-compose up
  3. Docker gestirà automaticamente il merge tra docker-compose.yml e docker-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

Segnale
Significato
Comportamento atteso

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

Verifica
Descrizione
Esito

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

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

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

Metodo
Contesto di utilizzo

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.

  • 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

Campo
Obbligatorio
Descrizione

level

Definisce il tipo di log (INFO, ERROR, DEBUG, CRITICAL, WARNING, ecc.)

timestamp

In UTC, formato RFC 3339, con T e Z maiuscoli

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

Level
Description
Required

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

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

Tipo
Obbligatorio
Descrizione

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

Elemento
Regola

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:

Test 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?