Salta al contenuto principale

03 - Golden Paths: Definizione e Implementazione di Flussi Standardizzati

Un golden path è un template supportato, non un obbligo. Ne costruisco uno con Cookiecutter e Backstage e lo controllo con un test eseguibile.

Cosa ne pensi di questo articolo?

03 - Golden Paths: Definizione e Implementazione di Flussi Standardizzati
Platform EngineeringFoto:Ruben BoekelooPexels

In Breve

Un golden path è un template supportato, non un obbligo. Ne costruisco uno con Cookiecutter e Backstage e lo controllo con un test eseguibile.

Che cos'è un golden path e che cosa promette

Un golden path è il percorso «con opinione e supportato» per costruire qualcosa: un servizio, un sito, una pipeline di dati. Questa è la definizione che ne dà Spotify, dove il concetto è nato come un tutorial scritto in una Hack Week per i servizi di backend, quando l'unico modo di scoprire come fare una cosa era chiederlo a un collega, pratica che il post chiama «rumour-driven development». Il documento del CNCF sulle piattaforme lo riduce a un oggetto concreto: un template di progetto iniziale insieme alla sua documentazione.

Il patto è nascosto in una parola, «supportato». Chi resta sul percorso ha l'aiuto del team di piattaforma; chi ne esce può farlo, ma sa che quel supporto non c'è più. Per questo non è una gabbia: nel post di Spotify l'obiettivo dichiarato non è limitare gli ingegneri né fissare standard fini a sé stessi, ma evitare che ogni team reinventi la ruota e abbia meno decisioni da prendere. Il rapporto DORA 2024 descrive i golden path come flussi di lavoro molto automatizzati e in self-service, e fra i compiti che li usano elenca la creazione di una nuova applicazione, il provisioning di un database, i test e la gestione del DNS.

In questo articolo costruisco un golden path vero, piccolo ma eseguibile: un template Cookiecutter che genera un servizio, la stessa cosa descritta come template di Backstage, e un test che controlla che il template produca un progetto sano. Su Backstage, il portale che li espone, si concentra l'articolo 4; per la domanda «quale golden path costruire per primo» vale lo script domanda-offerta dell'articolo 1. Dove il golden path si colloca fra i tre piani della piattaforma lo spiega l'articolo 2.

Quale golden path costruire: uno per tipo, a partire dal più frequente

Un golden path per tutto non esiste: un servizio HTTP, un consumatore di eventi e un modulo di infrastruttura hanno strutture diverse. Le categorie che compaiono più spesso sono queste:

  • Servizio HTTP: codice, controllo di salute, pipeline, descrittore di catalogo.
  • Applicazione web: build del frontend, pubblicazione, cache.
  • Consumatore di eventi: gestione dei tentativi e dei messaggi non elaborabili.
  • Job pianificato: pianificazione, log strutturati, allarme sul fallimento.
  • Modulo di infrastruttura: test, documentazione, versioni semantiche.

L'elenco non è un piano di lavoro: è il vocabolario con cui chiedere ai team che cosa creano davvero. Si parte da un solo percorso, quello che copre più servizi nuovi (un conteggio che hai già, nel catalogo o nel tracker delle richieste), e si aggiunge il secondo solo quando il primo ha utenti e un proprietario. Un golden path senza proprietario invecchia subito: la scelta di quale costruire per primo è anche la scelta di chi lo mantiene.

Il template: la definizione e lo scheletro

Cookiecutter è uno strumento Python (non un pacchetto npm: l'invocazione npx cookiecutter non esiste) che genera un progetto da una cartella di template. La versione corrente al 2026-11-13 è la 2.7.1, compatibile con Python da 3.10 a 3.14. Il file cookiecutter.json è la definizione del golden path: dichiara le domande da fare e i valori predefiniti.

{
  "service_name": "servizio-esempio",
  "owner": "team-checkout",
  "database": ["postgresql", "none"]
}

Nel file, service_name e owner sono testi liberi con un valore predefinito, mentre database è una lista: Cookiecutter la tratta come una scelta fra opzioni (le «choice variables» della documentazione) e usa la prima come predefinita. Con --no-input nessuna domanda viene posta e si usano i valori passati sulla riga di comando, oppure i predefiniti. È il comportamento che serve a una pipeline, dove nessuno può rispondere.

La cartella del template si chiama {{cookiecutter.service_name}}: il nome del servizio entra nel percorso, e dentro ogni file i segnaposto Jinja vengono sostituiti. Il file più interessante è il descrittore di catalogo, perché usa anche un blocco condizionale: la dipendenza dal database compare solo se la scelta non è none. Qui è riportato prima il template e poi il risultato della resa con database=postgresql.

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: {{ cookiecutter.service_name }}
  description: Servizio creato dal golden path "servizio-http"
spec:
  type: service
  lifecycle: experimental
  owner: {{ cookiecutter.owner }}
{%- if cookiecutter.database != "none" %}
  dependsOn:
    - resource:{{ cookiecutter.service_name }}-db
{%- endif %}

# Output (resa con service_name=pagamenti, owner=team-checkout, database=postgresql):
# apiVersion: backstage.io/v1alpha1
# kind: Component
# metadata:
#   name: pagamenti
#   description: Servizio creato dal golden path "servizio-http"
# spec:
#   type: service
#   lifecycle: experimental
#   owner: team-checkout
#   dependsOn:
#     - resource:pagamenti-db

Il segno meno in {%- if ... %} toglie la riga vuota che il blocco altrimenti lascerebbe nel YAML, e senza di esso il file resterebbe valido ma sgradevole. Gli altri quattro file dello scheletro sono un README.md, un Dockerfile che esegue come utente non privilegiato, un piccolo server HTTP con un percorso /health e una pipeline GitHub Actions che compila il codice. Non ho eseguito il Dockerfile (nessun Docker in questa macchina), né avviato il server (nessuna porta aperta), né lanciato la pipeline: ho verificato che il codice compili e che il YAML della pipeline sia leggibile. La generazione vera e propria produce questo:

cookiecutter template --no-input -o generato service_name=pagamenti owner=team-checkout database=postgresql

# Output (cd generato && find . -type f | LC_ALL=C sort):
# ./pagamenti/.github/workflows/ci.yml
# ./pagamenti/Dockerfile
# ./pagamenti/README.md
# ./pagamenti/app/main.py
# ./pagamenti/catalog-info.yaml

Lo stesso percorso nel portale: il template di Backstage

Lo scheletro da solo richiede che lo sviluppatore installi Cookiecutter, lo lanci e poi crei il repository, lo configuri e registri il componente nel catalogo. Il Software Template di Backstage compie questi passi dietro un modulo web. Il file seguente descrive gli stessi dati (nome, proprietario, database) e tre azioni in sequenza: generare lo scheletro, pubblicare il repository, registrare la voce di catalogo.

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: servizio-http
  title: Servizio HTTP
  description: Crea un servizio con repository, pipeline e voce di catalogo
  tags:
    - recommended
spec:
  owner: group:default/platform-team
  type: service

  parameters:
    - title: Dati del servizio
      required: [name, owner]
      properties:
        name:
          title: Nome del servizio
          type: string
          pattern: "^[a-z][a-z0-9-]*$"
          description: Minuscole, cifre e trattini
        owner:
          title: Team proprietario
          type: string
          ui:field: OwnerPicker
    - title: Opzioni tecniche
      properties:
        database:
          title: Database
          type: string
          enum: [postgresql, none]
          default: postgresql

  steps:
    - id: fetch
      name: Genera lo scheletro
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
          database: ${{ parameters.database }}

    - id: publish
      name: Pubblica il repository
      action: publish:github
      input:
        repoUrl: github.com?owner=example-org&repo=${{ parameters.name }}

    - id: register
      name: Registra nel catalogo
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: Repository
        url: ${{ steps.publish.output.remoteUrl }}

Tre punti del file meritano attenzione. Il parametro name ha un pattern che rifiuta i nomi non validi già nel modulo, prima che il template parta. Il campo owner usa il selettore OwnerPicker, che propone i gruppi del catalogo invece di un campo libero. Nei passi, ${{ parameters.name }} inserisce il valore del modulo (la sintassi è un sottoinsieme di Nunjucks), mentre ${{ steps.publish.output.repoContentsUrl }} prende l'uscita del passo precedente: per questo il passo register sa dove trovare il descrittore appena pubblicato.

Un'avvertenza di installazione dalla documentazione: l'azione publish:github non fa parte delle azioni incluse, ma del modulo @backstage/plugin-scaffolder-backend-module-github, che va aggiunto al backend. repoUrl ha la forma github.com?owner=...&repo=... e example-org qui è un nome inventato. Passare tutti i parametri allo scheletro (database compreso) è necessario perché lo scheletro li usi: un parametro dichiarato e non passato resta senza effetto.

Per non scoprire gli errori al primo uso, lo script seguente valida il template con il validatore ufficiale templateEntityV1beta3Validator del pacchetto @backstage/plugin-scaffolder-common, valida con componentEntityV1alpha1Validator il descrittore prodotto da Cookiecutter, e aggiunge un controllo che il validatore non fa: che ogni parameters.X usato nei passi sia dichiarato. Sotto lo script, due esecuzioni: la prima sui file corretti, la seconda su due copie rovinate a mano (un parametro scritto db invece di database e un descrittore senza owner).

// Valida entità Backstage con i validatori ufficiali del pacchetto catalog-model
// e controlla che ogni riferimento a "parameters.X" di un template esista.
const fs = require("fs");
const path = require("path");
const mod = (n) => require(path.join(__dirname, "node_modules", n));
const YAML = mod("yaml");
const { componentEntityV1alpha1Validator: componente } = mod("@backstage/catalog-model");
const { templateEntityV1beta3Validator: template } = mod("@backstage/plugin-scaffolder-common");

async function valida(file) {
  const documenti = YAML.parseAllDocuments(fs.readFileSync(file, "utf8")).map((d) => d.toJS());
  for (const e of documenti) {
    const validatore = e.kind === "Template" ? template : componente;
    try {
      const ok = await validatore.check(e);
      console.log(`${file}: ${e.kind} ${e.metadata.name} -> ${ok ? "valido" : "kind non gestito"}`);
    } catch (err) {
      console.log(`${file}: ${e.kind} ${e.metadata.name} -> NON valido: ${err.message}`);
      process.exitCode = 1;
    }
    if (e.kind === "Template") {
      const dichiarati = new Set(e.spec.parameters.flatMap((p) => Object.keys(p.properties || {})));
      const usati = [...JSON.stringify(e.spec.steps).matchAll(/parameters\.(\w+)/g)].map((m) => m[1]);
      const mancanti = [...new Set(usati)].filter((n) => !dichiarati.has(n));
      console.log(`  parametri dichiarati: ${[...dichiarati].join(", ")}; non dichiarati ma usati: ${mancanti.join(", ") || "nessuno"}`);
      if (mancanti.length) process.exitCode = 1;
    }
  }
}

(async () => { for (const f of process.argv.slice(2)) await valida(f); })();

// Output:
// $ node valida.cjs template-backstage.yaml generato/pagamenti/catalog-info.yaml
// template-backstage.yaml: Template servizio-http -> valido
//   parametri dichiarati: name, owner, database; non dichiarati ma usati: nessuno
// generato/pagamenti/catalog-info.yaml: Component pagamenti -> valido
// (codice di uscita: 0)
// $ node valida.cjs template-sbagliato.yaml catalog-sbagliato.yaml
// template-sbagliato.yaml: Template servizio-http -> valido
//   parametri dichiarati: name, owner, database; non dichiarati ma usati: db
// catalog-sbagliato.yaml: Component pagamenti -> NON valido: /spec must have required property 'owner' - missingProperty: owner
// (codice di uscita: 1)

Il risultato più istruttivo è il secondo: il template con il parametro sbagliato è per il validatore «valido», perché lo schema controlla la struttura e non il contenuto delle espressioni, ed è il controllo aggiuntivo a segnalare db. Il descrittore senza proprietario, invece, è respinto dallo schema stesso con /spec must have required property 'owner'. Il codice di uscita 1 è ciò che permette di mettere lo script in una pipeline. I pacchetti usati sono @backstage/catalog-model 1.10.2 e @backstage/plugin-scaffolder-common 2.3.1, installati in una cartella temporanea.

Governare i template: un test che genera e controlla

Un template è software con utenti, quindi si versiona e si testa. Cinque pratiche separano un template mantenuto da uno abbandonato:

  • Versioni semantiche: ogni rilascio ha un numero, e i cambiamenti incompatibili sono dichiarati.
  • Revisione: chi modifica il template lo fa con una revisione del team di piattaforma e di chi lo usa.
  • Test di generazione: il template si genera in una cartella temporanea e si controlla il risultato.
  • Ritiro dichiarato: un template obsoleto ha una data e un percorso di migrazione.
  • Registro dei cambiamenti: ogni versione spiega che cosa cambia e perché.

L'unica voce che si può automatizzare del tutto è la terza, ed è la più trascurata. Il test seguente genera il template con Cookiecutter, in una cartella temporanea, e controlla che i cinque file obbligatori esistano, che non restino segnaposto non risolti, che il descrittore riporti il nome e il proprietario richiesti, che il YAML della pipeline sia leggibile e che il codice compili. Eseguito su un template sano e su due rovinati (uno senza descrittore e con un errore di sintassi, uno che usa una variabile mai dichiarata), segnala tutto.

"""Test di conformità di un template: lo genera e controlla che il risultato sia un progetto sano."""
import py_compile
import re
import sys
import tempfile
from pathlib import Path

import yaml
from cookiecutter.exceptions import CookiecutterException
from cookiecutter.main import cookiecutter

OBBLIGATORI = ["README.md", "Dockerfile", "catalog-info.yaml", "app/main.py", ".github/workflows/ci.yml"]
RESIDUO = re.compile(r"\{\{|\{%")


def verifica(template, nome="pagamenti", owner="team-checkout", database="postgresql"):
    errori = []
    with tempfile.TemporaryDirectory() as tmp:
        try:
            cookiecutter(str(template), no_input=True, output_dir=tmp, extra_context={
                "service_name": nome, "owner": owner, "database": database})
        except CookiecutterException as e:
            return [f"generazione fallita: {type(e).__name__}"]
        radice = Path(tmp) / nome
        for rel in OBBLIGATORI:
            if not (radice / rel).is_file():
                errori.append(f"manca {rel}")
        for f in radice.rglob("*"):
            if f.is_file() and RESIDUO.search(f.read_text(encoding="utf-8")):
                errori.append(f"segnaposto non risolto in {f.relative_to(radice)}")
        catalogo = radice / "catalog-info.yaml"
        if catalogo.is_file():
            c = yaml.safe_load(catalogo.read_text(encoding="utf-8"))
            if c["metadata"]["name"] != nome:
                errori.append("catalog-info: il nome non coincide con il servizio")
            if c["spec"]["owner"] != owner:
                errori.append("catalog-info: proprietario non valorizzato")
        ci = radice / ".github/workflows/ci.yml"
        if ci.is_file():
            yaml.safe_load(ci.read_text(encoding="utf-8"))
        main = radice / "app/main.py"
        if main.is_file():
            try:
                py_compile.compile(str(main), doraise=True, cfile=str(Path(tmp) / "main.pyc"))
            except py_compile.PyCompileError:
                errori.append("app/main.py non compila")
    return errori


if __name__ == "__main__":
    problemi = verifica(Path(sys.argv[1]))
    print("template conforme" if not problemi else "NON conforme: " + "; ".join(problemi))
    sys.exit(1 if problemi else 0)

# Output:
# python3 verifica_template.py template
# template conforme
# (codice di uscita: 0)
# python3 verifica_template.py template-rotto
# NON conforme: manca catalog-info.yaml; app/main.py non compila
# (codice di uscita: 1)
# python3 verifica_template.py template-rotto2
# NON conforme: generazione fallita: UndefinedVariableInTemplate
# (codice di uscita: 1)

Notare che Cookiecutter, di suo, si ferma con UndefinedVariableInTemplate se un file usa una variabile non dichiarata: il test cattura l'eccezione e la riporta come «non conforme» invece di farsi interrompere. Questo controllo evita il caso peggiore, un golden path che fallisce per tutti al primo uso, ma non dice nulla sulla qualità del progetto generato.

Resta un problema che nessun test del template risolve: i progetti già generati non si aggiornano quando il template cambia, perché sono copie. Esiste uno strumento costruito sopra Cookiecutter, cruft, che permette di controllare e applicare gli aggiornamenti di un template a un progetto esistente (cruft check e cruft update); il suo repository risulta però senza commit da dicembre 2024 (dato letto il 2026-11-13), quindi va valutato prima di fondarci un processo. In alternativa si pubblicano le modifiche come richieste di modifica automatiche, o si accetta che il template valga solo per i servizi nuovi.

Misurare l'adozione senza imporla

Un golden path funziona se viene scelto. Quattro segnali lo dicono, e sono raccolti dal catalogo e dal repository:

  • Quota di servizi nuovi creati dal template, contata sul catalogo (il descrittore generato porta la traccia).
  • Tempo dal primo commit al primo rilascio in staging, per i servizi nati dal template.
  • Deviazioni e motivi: quanti servizi nascono fuori dal percorso, e che cosa dicono i loro autori.
  • Età media del template e data dell'ultimo aggiornamento.

Le cifre che vedi spesso nelle presentazioni («adozione sopra l'80%», «onboarding ridotto dell'80%») non hanno una fonte primaria e non le riporto. Dai dati del rapporto DORA 2024 la lezione è un'altra: chi è obbligato a usare la piattaforma in esclusiva per tutto il ciclo di vita dell'applicazione ha un throughput inferiore del 6%, mentre il guadagno di produttività (+5%) viene dall'indipendenza degli sviluppatori. Una deviazione frequente è un'informazione sul percorso, non una colpa dei team. Le metriche che la piattaforma deve guardare sono nell'articolo 7 e l'ascolto qualitativo degli sviluppatori nell'articolo 9.

Trappole

Quattro trappole ricorrenti, tutte giudizi di esperienza e non risultati di ricerca:

  • Percorso obbligatorio e rigido: trasforma il golden path in un collo di bottiglia.
  • Template che invecchia: i progetti nuovi partono con dipendenze già superate.
  • Documentazione assente: lo scheletro funziona, ma nessuno sa perché è fatto così.
  • Un solo template per tutto: più opzioni, più rami, più test da scrivere.

La regola che ne ricavo è dello stesso tipo: il miglior golden path è quello che gli sviluppatori scelgono perché è il più comodo. Se per usarlo serve un mandato, il difetto sta nel percorso. È la distinzione del modello di maturità del CNCF fra adozione «spinta da fuori» e adozione «tirata dagli utenti».

Che cosa sai fare adesso

Sai definire un golden path come un template con domande e valori predefiniti, generarlo senza input e verificarne il risultato con un test eseguibile in pipeline. Sai descrivere lo stesso percorso come template di Backstage, validarlo con i validatori ufficiali e controllare anche i riferimenti ai parametri. Sai dire quali misure dicono se il percorso viene scelto, e quali cifre non vanno riportate senza fonte. Il prossimo articolo passa al portale che li espone, Backstage.

Domande frequenti

Che cos'è un golden path?

È il percorso con opinione e supportato per costruire qualcosa: per il CNCF un template di progetto con la sua documentazione, per Spotify il modo raccomandato di usare gli strumenti dell'azienda. Chi ne esce può farlo, ma perde il supporto del team di piattaforma.

Un golden path è obbligatorio?

No, e non conviene che lo sia. Il rapporto DORA 2024 trova un throughput inferiore del 6% per chi è obbligato a usare la piattaforma in esclusiva per tutto il ciclo di vita dell'applicazione. Il percorso si rende conveniente, non obbligatorio.

Come si testa un template di progetto?

Lo si genera in una cartella temporanea e si controlla il risultato: file obbligatori presenti, nessun segnaposto non risolto, descrittore di catalogo coerente, YAML leggibile, codice che compila. Lo script dell'articolo lo fa con Cookiecutter e segnala anche la generazione fallita.

Che cosa succede ai progetti già generati quando il template cambia?

Non cambiano: sono copie. Si può usare uno strumento come cruft per controllare e applicare gli aggiornamenti, pubblicare modifiche automatiche sui repository, oppure accettare che il template valga solo per i servizi nuovi.

Nella stessa serie trovi anche la sicurezza con Zero Trust e le policy, il piano di implementazione di una IDP e il confronto fra AI on-prem e cloud.

Fonti

Gli script sono stati eseguiti due volte con uscita identica (Python 3.14, Cookiecutter 2.7.1 e Jinja2 3.1.6 in un ambiente virtuale temporaneo; Node 24 con i pacchetti Backstage indicati). Dockerfile, server e pipeline GitHub Actions generati non sono stati eseguiti. I nomi di organizzazione e di team sono inventati.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti