Hai Kubernetes in produzione. I tuoi deployment girano, i pod scalano, il cluster regge. Eppure ogni volta che uno sviluppatore vuole deployare una nuova app, apre una PR sul repo delle configurazioni, aspetta che qualcuno del team platform la riveda, e nel frattempo ti scrive su Slack per chiederti "ma quando è che arriva il mio namespace?".
Suona familiare?
Oggi questo modello non è più sostenibile. Le aziende medio-grandi che hanno abbracciato DevOps negli anni scorsi si trovano ora di fronte a un problema di scala: il team platform è diventato un collo di bottiglia umano. La risposta dell'industria si chiama Platform Engineering, e il suo strumento centrale è l'Internal Developer Platform (IDP).
In questo articolo vedremo come costruire una IDP concreta con Kubernetes e Backstage: dai golden path ai template Software Templates, fino alle policy di governance con OPA e Kyverno. Niente teoria — solo roba che puoi applicare da subito.
Cos'è una Internal Developer Platform (e perché non è "solo un portale")
Prima di sporcarci le mani, chiarisco un equivoco comune: un'IDP non è un bel frontend sopra kubectl. È un sistema che astrae la complessità dell'infrastruttura e offre ai developer un'esperienza self-service con guardrail integrati.
I tre pilastri di un'IDP matura sono:
- Self-service: il developer ottiene ciò di cui ha bisogno senza aprire ticket
- Golden path: percorsi "opinionated" che incorporano best practice di sicurezza, osservabilità e governance
- Ownership chiara: ogni servizio ha un owner, una documentazione e un contesto raggiungibili dallo stesso posto
Backstage — il progetto open source di Spotify, ora parte della CNCF — è diventato lo standard de facto per la UI e il catalogo dell'IDP. Kubernetes è il runtime su cui tutto gira. Messi insieme, danno vita a una piattaforma che scala con la tua organizzazione.
Architettura di riferimento
Prima di entrare nei dettagli, ecco lo stack che costruiremo:
Backstage gestisce l'esperienza developer (catalogo, template, doc). Crossplane o ArgoCD gestiscono il provisioning e il delivery. OPA/Kyverno garantiscono che nessun team bypasso le policy.
Step 1: installare Backstage e configurare il Software Catalog
Partiamo dall'installazione di Backstage. Avrai bisogno di Node.js 20+ e npx:
npx @backstage/create-app@latest
cd my-backstage-app
yarn install
yarn dev
Il Software Catalog è il cuore di Backstage. Ogni servizio, API, libreria o risorsa viene registrata tramite un file catalog-info.yaml nel repo del progetto:
# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Microservizio gestione ordini
annotations:
backstage.io/techdocs-ref: dir:.
github.com/project-slug: myorg/order-service
prometheus.io/alert: "AlertManager"
tags:
- java
- spring-boot
- production
links:
- url: https://grafana.mycompany.it/d/order-service
title: Dashboard Grafana
icon: dashboard
spec:
type: service
lifecycle: production
owner: team-backend
system: order-management
providesApis:
- order-api
consumesApis:
- inventory-api
- payment-api
Puoi configurare Backstage per auto-scoprire questi file via GitHub/GitLab integration. In app-config.yaml:
catalog:
providers:
github:
myorg:
organization: 'myorg'
catalogPath: '/catalog-info.yaml'
filters:
branch: 'main'
rules:
- allow: [Component, System, API, Resource, Location]
Risultato: in pochi minuti hai un catalogo unificato di tutti i tuoi servizi, con owner, dipendenze e link alle risorse operative — tutto in un posto.
Step 2: creare Golden Path con i Software Templates
I Software Templates (o "Scaffolder") sono il meccanismo con cui un developer crea una nuova app seguendo il golden path della tua organizzazione: struttura del repo, Dockerfile, pipeline CI/CD, configurazione Kubernetes — tutto pre-cablato.
Ecco un template per un microservizio Spring Boot:
# templates/spring-boot-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: spring-boot-microservice
title: Spring Boot Microservice
description: Crea un nuovo microservizio Spring Boot con CI/CD e monitoring inclusi
tags:
- java
- spring-boot
- recommended
spec:
owner: team-platform
type: service
parameters:
- title: Informazioni servizio
required:
- name
- description
- owner
properties:
name:
title: Nome servizio
type: string
pattern: '^[a-z][a-z0-9-]*$'
description: Nome in kebab-case (es. order-service)
description:
title: Descrizione
type: string
owner:
title: Team owner
type: string
ui:field: OwnerPicker
ui:options:
allowedKinds: ['Group']
- title: Infrastruttura
properties:
replicas:
title: Numero di repliche iniziali
type: integer
default: 2
enum: [1, 2, 3, 5]
resourceProfile:
title: Profilo risorse
type: string
default: small
enum: [small, medium, large]
enumNames:
- 'Small (256m CPU, 256Mi RAM)'
- 'Medium (500m CPU, 512Mi RAM)'
- 'Large (1 CPU, 1Gi RAM)'
steps:
- id: fetch-base
name: Fetch base template
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
replicas: ${{ parameters.replicas }}
resourceProfile: ${{ parameters.resourceProfile }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ['github.com']
description: ${{ parameters.description }}
repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
defaultBranch: main
repoVisibility: private
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
- id: create-argocd-app
name: Crea applicazione ArgoCD
action: http:backstage:request
input:
method: POST
path: /api/proxy/argocd/api/v1/applications
body:
metadata:
name: ${{ parameters.name }}
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/${{ parameters.name }}
targetRevision: HEAD
path: k8s/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: ${{ parameters.name }}
output:
links:
- title: Repository GitHub
url: ${{ steps['publish'].output.remoteUrl }}
- title: Catalogo Backstage
url: ${{ steps['register'].output.catalogInfoUrl }}
La cartella skeleton/ contiene la struttura del progetto con variabili Nunjucks. Esempio per il catalog-info.yaml generato:
# skeleton/catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.name }}
description: ${{ values.description }}
spec:
type: service
lifecycle: experimental
owner: ${{ values.owner }}
Con questo template, un developer compila un form in Backstage e in 2 minuti ha: repo GitHub con struttura standard, pipeline GitHub Actions pre-configurata, Deployment Kubernetes con le risorse giuste, e app registrata nel catalogo. Zero Slack, zero PR sul repo platform.
Step 3: Namespace e RBAC automatizzati con Crossplane
Ogni nuovo servizio ha bisogno di un namespace Kubernetes con RBAC e network policy corrette. Farlo a mano è il classico collo di bottiglia. Con Crossplane puoi definire un Composite Resource (XR) che incapsula tutta la logica:
# platform/compositions/app-namespace.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-namespace
spec:
compositeTypeRef:
apiVersion: platform.mycompany.it/v1alpha1
kind: AppNamespace
resources:
- name: namespace
base:
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
spec:
forProvider:
manifest:
apiVersion: v1
kind: Namespace
metadata:
labels:
managed-by: crossplane
team: "" # patch dal claim
patches:
- fromFieldPath: spec.parameters.team
toFieldPath: spec.forProvider.manifest.metadata.labels.team
- name: resource-quota
base:
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
spec:
forProvider:
manifest:
apiVersion: v1
kind: ResourceQuota
metadata:
name: default-quota
spec:
hard:
requests.cpu: "4"
requests.memory: 4Gi
limits.cpu: "8"
limits.memory: 8Gi
pods: "20"
patches:
- fromFieldPath: spec.parameters.namespace
toFieldPath: spec.forProvider.manifest.metadata.namespace
- fromFieldPath: spec.parameters.resourceProfile
toFieldPath: spec.forProvider.manifest.spec.hard
transforms:
- type: map
map:
small:
requests.cpu: "2"
limits.cpu: "4"
requests.memory: 2Gi
limits.memory: 4Gi
medium:
requests.cpu: "4"
limits.cpu: "8"
requests.memory: 4Gi
limits.memory: 8Gi
- name: network-policy-default-deny
base:
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
spec:
forProvider:
manifest:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
Il developer (o il template Backstage) crea semplicemente un claim:
# Claim: l'unica cosa che vede il developer
apiVersion: platform.mycompany.it/v1alpha1
kind: AppNamespace
metadata:
name: order-service-ns
spec:
parameters:
namespace: order-service
team: team-backend
resourceProfile: medium
Crossplane crea namespace, ResourceQuota e NetworkPolicy in automatico. Il developer non tocca mai le risorse Kubernetes direttamente.
Step 4: Policy di Governance con Kyverno
Il self-service funziona solo se hai guardrail solidi. Kyverno ti permette di definire policy Kubernetes che vengono valutate a ogni richiesta all'API server — in modalità enforce (blocca) o audit (logga senza bloccare).
Ecco alcune policy fondamentali:
Require resource limits su ogni container
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
annotations:
policies.kyverno.io/title: Require Resource Limits
policies.kyverno.io/description: >
Ogni container deve dichiarare CPU e memory limits.
Senza limiti un pod può esaurire le risorse del nodo.
spec:
validationFailureAction: enforce
background: true
rules:
- name: validate-limits
match:
any:
- resources:
kinds:
- Pod
namespaceSelector:
matchLabels:
managed-by: crossplane # Solo nei namespace gestiti dalla piattaforma
validate:
message: "CPU e memory limits sono obbligatori per ogni container."
pattern:
spec:
containers:
- resources:
limits:
cpu: "?*"
memory: "?*"
Blocca immagini senza tag (no latest)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: enforce
rules:
- name: require-image-tag
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Usare il tag ':latest' è vietato. Specifica una versione esplicita."
pattern:
spec:
containers:
- image: "!*:latest"
initContainers:
- image: "!*:latest"
Mutate: aggiungi label standard automaticamente
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-standard-labels
spec:
rules:
- name: add-team-label
match:
any:
- resources:
kinds: [Pod, Deployment, Service]
mutate:
patchStrategicMerge:
metadata:
labels:
+(managed-by): crossplane
+(platform-version: "1.0"
Con Kyverno in audit mode puoi capire quante violazioni esistono prima di passare a enforce. Il comando per vedere il report:
kubectl get policyreport -A
kubectl describe clusterpolicyreport -n order-service
Step 5: Osservabilità integrata nel Golden Path
Un golden path serio include l'osservabilità di default. Ogni app generata dal template deve avere metriche Prometheus e log strutturati senza che il developer debba configurare nulla.
Nel template Backstage, includi già nella skeleton un ServiceMonitor:
# skeleton/k8s/base/service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ${{ values.name }}
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: ${{ values.name }}
endpoints:
- port: http
path: /actuator/prometheus
interval: 30s
scrapeTimeout: 10s
E un dashboard Grafana pre-configurato tramite ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: ${{ values.name }}-dashboard
labels:
grafana_dashboard: "1"
data:
dashboard.json: |
{
"title": "${{ values.name }} - Overview",
"panels": [
{
"title": "Request Rate",
"targets": [{ "expr": "rate(http_requests_total{app=\"${{ values.name }}\"}[5m])" }]
},
{
"title": "Error Rate",
"targets": [{ "expr": "rate(http_requests_total{app=\"${{ values.name }}\",status=~\"5..\"}[5m])" }]
},
{
"title": "P99 Latency",
"targets": [{ "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{app=\"${{ values.name }}\"}[5m]))" }]
}
]
}
Il developer ottiene il suo Grafana dashboard dal giorno zero. Senza configurare nulla.
Governance e adozione: i soft skills del Platform Engineering
La tecnologia è la parte facile. Il vero lavoro è l'adozione.
Alcune lezioni pratiche:
Tratta i developer come clienti. Il tuo "prodotto" è la piattaforma. Se i golden path sono scomodi, i team li bypasseranno. Raccogli feedback regolarmente, misura il tempo medio per deployare un nuovo servizio (Time to First Deploy), e traccia le pull request "fuori standard".
Parti dall'audit mode. Non passare a enforce su Kyverno finché non hai misurato quante violazioni esistono nell'attuale parco applicativo. Cominciare con il blocco crea ostilità.
Documenta nel catalogo, non su Confluence. TechDocs in Backstage permette di tenere la documentazione vicina al codice. Se la doc sta nel repo, viene aggiornata. Se sta su Confluence, invecchia.
Versiona la tua piattaforma. Tratta l'IDP come un prodotto con release notes. Quando aggiorni un template o una policy, comunicalo.
Conclusione
Il Platform Engineering non è una moda: è la risposta strutturale ai limiti del DevOps tradizionale quando la scala aumenta. Kubernetes e Backstage, messi insieme con Crossplane e Kyverno, ti danno tutti gli strumenti per costruire una IDP che scala con la tua organizzazione.
Il punto di partenza è sempre lo stesso: individua il collo di bottiglia più grande (spesso è il provisioning dei namespace o il setup iniziale di un repo), costruisci il golden path per quel caso, e misura il prima e il dopo. Poi iteri.
La piattaforma non finisce mai — e questa è la cosa più bella del Platform Engineering.
Hai già una IDP in produzione? Raccontami la tua esperienza nei commenti — o scrivimi direttamente. Ogni architettura è diversa, e confrontarsi con chi è già sulla strada è sempre il modo più veloce per migliorare.
Lascia un commento