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
- Spotify Engineering: How We Use Golden Paths to Solve Fragmentation in Our Software Ecosystem - definizione «opinionated and supported», origine in una Hack Week, scopo dichiarato, consultata il 2026-11-13
- CNCF Platforms White Paper - golden path come template iniziale più documentazione, consultata il 2026-11-13
- Accelerate State of DevOps 2024 (DORA) - golden path come flussi automatizzati in self-service, -6% di throughput per l'uso esclusivo, +5% per l'indipendenza, pp. 48-53, consultata il 2026-11-13
- Backstage: Writing Templates - struttura di un template v1beta3,
OwnerPicker,catalog:register, sintassi Nunjucks, consultata il 2026-11-13 - Backstage: Builtin Actions - i moduli di azione da aggiungere, fra cui quello di GitHub, consultata il 2026-11-13
- Cookiecutter: Choice Variables - le variabili a scelta in
cookiecutter.json, consultata il 2026-11-13 - cruft -
cruft checkecruft updatesopra Cookiecutter; ultimo commit del 2024-12-25, consultata il 2026-11-13
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