Kubernetes non costa troppo perché Kubernetes è inefficiente. Costa troppo quando nessuno misura cosa viene richiesto, cosa viene realmente usato, quali nodi restano mezzi vuoti, quali namespace non hanno ownership e quali risorse continuano a vivere anche quando il servizio che le usava è sparito.
Il problema non è solo tecnico. È operativo. Se un team può creare namespace, deployment, PVC, job e ambienti temporanei senza limiti, senza label e senza revisione periodica, il cluster diventa lentamente una somma di piccole decisioni invisibili. Nessuna di queste sembra grave da sola. Tutte insieme diventano bolletta, complessità e rischio.
Da dove iniziare: non ottimizzare prima di misurare
La prima domanda non è “quale autoscaler installiamo?”. La prima domanda è: quanto costa ogni namespace, team, ambiente e workload? Senza questa vista si finisce per ottimizzare a sensazione, magari intervenendo su componenti poco rilevanti mentre il grosso dello spreco resta altrove.
Una base minima dovrebbe includere:
- namespace e workload etichettati con
team,environment,serviceeowner; - metriche reali di CPU e memoria, idealmente con Prometheus o metrica equivalente del provider;
- strumenti di cost allocation come OpenCost o Kubecost;
- report periodico per distinguere produzione, staging, sviluppo, batch e workload temporanei;
- una persona o un team responsabile per ogni namespace.
Una policy semplice può essere più utile di un progetto FinOps complesso:
metadata:
labels:
owner: "platform"
team: "payments"
environment: "production"
service: "checkout"
Se non sai chi possiede una risorsa, non puoi sapere chi deve approvare il costo, chi può ridurlo e chi risponde se quella risorsa viene rimossa per errore.
1. Requests: il punto in cui nasce gran parte dello spreco
In Kubernetes le requests non sono un consiglio. Sono la quantità di CPU e memoria che il scheduler usa per decidere dove collocare i pod. Se un container usa in media 80 millicore ma richiede 500 millicore, il cluster prenota capacità che probabilmente non userai mai.
Questo produce un effetto molto comune: i nodi sembrano pieni dal punto di vista dello scheduler, ma sono semivuoti dal punto di vista dell'uso reale. Il cluster scala, nuovi nodi vengono creati, la fattura cresce, ma la CPU effettivamente consumata resta bassa.
Parti da una fotografia semplice:
kubectl top pods -A --sort-by=cpu
kubectl top pods -A --sort-by=memory
Poi confronta consumo reale e richieste dichiarate:
kubectl get pods -A \
-o custom-columns='NAMESPACE:.metadata.namespace,POD:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory'
Il dato più utile non è il picco di un minuto. Guarda p50, p90 e p95 su alcuni giorni o settimane. Per workload web stabili, una request ragionevole spesso nasce dal p90 o p95 più un margine. Per batch o workload molto variabili serve più cautela.
2. Usa VPA come consulente, non necessariamente come pilota automatico
Il Vertical Pod Autoscaler è utile perché analizza l'uso storico e produce raccomandazioni su CPU e memoria. Non significa che debba modificare automaticamente tutti i workload. In molti ambienti production conviene iniziare in modalità raccomandazione.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: checkout-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout
updatePolicy:
updateMode: "Off"
Dopo alcuni giorni:
kubectl describe vpa checkout-vpa
Usa i valori raccomandati come input per una pull request, non come verità assoluta. Il modello operativo migliore è: VPA suggerisce, il team valuta, GitOps applica. Così mantieni auditabilità e riduci il rischio di modifiche automatiche difficili da spiegare.
Attenzione anche alla relazione con HPA: HPA scala orizzontalmente in base all'utilizzo rispetto alle requests. Se le requests sono sbagliate, anche HPA prende decisioni sbagliate. Prima correggi le requests, poi valuta autoscaling orizzontale.
3. Limits: protezione, non magia
I limits servono a impedire che un container consumi risorse oltre una soglia. Sono importanti, ma vanno usati con criterio. Un limite CPU troppo basso può introdurre throttling e peggiorare la latenza. Un limite memoria troppo basso può generare OOMKill.
Una strategia prudente è:
- definire sempre requests per CPU e memoria;
- usare limiti memoria coerenti con il comportamento dell'applicazione;
- evitare limiti CPU troppo aggressivi su servizi latency-sensitive;
- imporre default e massimi per namespace tramite
LimitRange; - monitorare CPU throttling e OOMKill dopo ogni revisione.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: staging
spec:
limits:
- type: Container
defaultRequest:
cpu: "100m"
memory: "128Mi"
default:
cpu: "500m"
memory: "512Mi"
max:
cpu: "2"
memory: "2Gi"
Non usare gli stessi default per tutto. Un namespace di staging, un namespace di batch e un namespace production con servizi critici hanno profili diversi.
4. Namespace quota: bloccare gli incidenti di costo prima che accadano
Le ResourceQuota sono una cintura di sicurezza. Non ottimizzano automaticamente, ma impediscono che un errore o un esperimento diventino capacità allocata senza controllo.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-alpha-quota
namespace: team-alpha
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
count/pods: "40"
count/persistentvolumeclaims: "10"
Le quote funzionano bene quando sono accompagnate da un processo semplice: se un team ha bisogno di più capacità, apre una richiesta motivata. Questo non serve a rallentare il lavoro. Serve a rendere visibile una decisione economica e tecnica.
5. Nodi spot: risparmio reale, ma solo per workload adatti
I nodi spot o preemptible possono ridurre molto il costo, ma non sono una bacchetta magica. Possono essere terminati con breve preavviso e richiedono applicazioni capaci di tollerare interruzioni.
Buoni candidati:
- runner CI/CD;
- batch job idempotenti;
- pipeline dati ripetibili;
- servizi stateless con più repliche;
- ambienti development e staging.
Cattivi candidati:
- database e workload stateful non progettati per disruption;
- servizi con una sola replica;
- componenti critici senza retry, timeout e graceful shutdown;
- job lunghi non checkpointabili.
La disponibilità non viene dal fatto che il nodo costa meno. Viene da replica, distribuzione tra zone, topologySpreadConstraints, readiness probe, shutdown pulito e retry applicativi.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: spot-friendly
value: 100
globalDefault: false
description: "Workloads that can tolerate spot interruptions"
Etichette, taint e toleration aiutano a separare workload resilienti e workload critici. Non mischiare tutto nello stesso pool sperando che l'autoscaler faccia sempre la scelta giusta.
6. Autoscaling dei nodi: dal Cluster Autoscaler a Karpenter
Il Cluster Autoscaler resta una soluzione valida, ma ragiona soprattutto su node group. Strumenti più moderni come Karpenter su EKS possono creare nodi in modo più dinamico in base ai pod pendenti, scegliere istanze più adatte e consolidare capacità sottoutilizzata.
Il punto non è installare Karpenter e aspettarsi automaticamente il 40% di risparmio. Il punto è dargli libertà sufficiente per ottimizzare:
- non restringere eccessivamente le famiglie di istanze;
- separa pool on-demand e spot quando serve;
- imposta limiti di CPU/memoria per evitare crescita incontrollata;
- usa disruption budget per proteggere periodi e servizi critici;
- abilita il consolidamento solo dopo aver capito l'impatto sui workload.
Un autoscaler non corregge manifest sbagliati. Se richieste e vincoli sono irrealistici, anche il miglior autoscaler produrrà nodi costosi o frammentati.
7. Riduci gli ambienti non production
Development, test e staging sono spesso il punto più facile da ottimizzare. Molti ambienti non devono restare attivi 24/7. Se lavorano solo in orario ufficio, puoi scalare a zero o ridurre repliche durante notte e weekend.
apiVersion: batch/v1
kind: CronJob
metadata:
name: scale-down-staging
namespace: platform
spec:
schedule: "0 20 * * 1-5"
jobTemplate:
spec:
template:
spec:
serviceAccountName: staging-scaler
restartPolicy: OnFailure
containers:
- name: kubectl
image: bitnami/kubectl
command:
- kubectl
- scale
- deployment
- --all
- --replicas=0
- -n
- staging
Prima di farlo, chiarisci le eccezioni: demo environment, test notturni, ambienti usati da team in altri fusi orari, job schedulati. Scale-to-zero senza governance diventa un altro incidente.
8. Pulisci PVC, job e risorse orfane
Una parte dello spreco Kubernetes non è CPU. È storage, immagini, job completati, namespace dimenticati e risorse non più referenziate.
Per trovare PVC non montati da pod correnti:
kubectl get pvc --all-namespaces -o json \
| jq -r '.items[] | .metadata.namespace + "/" + .metadata.name' \
| sort -u > /tmp/all-pvcs.txt
kubectl get pods --all-namespaces -o json \
| jq -r '.items[] | .metadata.namespace as $ns | .spec.volumes[]?
| select(.persistentVolumeClaim) | $ns + "/" + .persistentVolumeClaim.claimName' \
| sort -u > /tmp/used-pvcs.txt
comm -23 /tmp/all-pvcs.txt /tmp/used-pvcs.txt
Non cancellare automaticamente il risultato. Un PVC può essere usato da un job schedulato o da una procedura di restore. Ma la lista è un buon punto di partenza per una review con gli owner.
Per i job completati, usa ttlSecondsAfterFinished dove appropriato:
apiVersion: batch/v1
kind: Job
metadata:
name: report-export
spec:
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: export
image: example/report-export:1.0.0
9. Osserva anche ciò che non scala: control plane, logging, egress
Quando si parla di costi Kubernetes si pensa subito ai nodi worker. Ma spesso il costo cresce anche altrove:
- log troppo verbosi inviati a sistemi di logging costosi;
- metriche ad alta cardinalità;
- egress tra zone, regioni o provider;
- load balancer creati per servizi temporanei;
- volumi con classi storage sovradimensionate;
- snapshot e backup senza retention.
Un cluster può avere nodi ben dimensionati e comunque costare troppo perché ogni pod produce log inutili, ogni ambiente crea load balancer dedicati e ogni volume resta in classe premium anche quando non serve.
10. Una routine mensile semplice
La differenza tra ottimizzazione e controllo sta nella ripetizione. Una routine mensile può bastare:
- top 10 namespace per costo;
- top 10 workload con request molto superiore all'uso reale;
- PVC non montati o storage cresciuto oltre soglia;
- namespace senza owner o senza label obbligatorie;
- nodi con utilizzo medio basso;
- workload non production attivi fuori orario;
- review delle raccomandazioni VPA;
- incidenti recenti legati a OOM, throttling o autoscaling.
Trasforma ogni punto in ticket piccoli. Non serve fare “il grande progetto di ottimizzazione” una volta l'anno. Serve impedire che il disordine si accumuli.
Errori comuni da evitare
- Tagliare requests senza guardare i picchi. Riduci costo oggi, crei instabilità domani.
- Mettere tutto su spot. Spot è utile, ma solo per workload progettati per interruzione.
- Confondere limite con sicurezza. Un limite CPU aggressivo può degradare il servizio.
- Ottimizzare senza owner. Se nessuno possiede una risorsa, nessuno può decidere davvero.
- Ignorare storage e logging. Non tutto il costo è nei nodi.
- Installare tool senza processo. OpenCost, VPA o Karpenter aiutano solo se qualcuno usa i dati per cambiare il sistema.
Conclusione
Ottimizzare Kubernetes non significa rendere il cluster “più piccolo” a tutti i costi. Significa usare capacità coerente con il valore prodotto. Alcuni workload meritano margine, ridondanza e nodi on-demand. Altri possono vivere su spot, scalare a zero o ricevere limiti più stretti.
La maturità sta nel distinguere. Misura prima, attribuisci ownership, correggi le requests, usa VPA come guida, applica quote, separa i workload resilienti, pulisci ciò che è orfano e trasforma il costo in un segnale operativo. Quando il costo diventa visibile, smette di essere una sorpresa e diventa una decisione tecnica.
Vuoi sperimentare AI locale con responsabilita?
Aiutiamo team e aziende a progettare ambienti AI locali, privati e governabili, bilanciando liberta tecnica, sicurezza, policy e controllo operativo.
Inizia la conversazione