KubernetesFinOpsDevOps

Ottimizzazione costi Kubernetes: guida pratica per il 2025

25 aprile 2025 · 14 min · Di Fabrizio Galiano

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.

Questa guida è pratica per un motivo preciso. L'obiettivo non è “spendere meno” in astratto, ma costruire un metodo: misurare, attribuire, correggere, automatizzare e mantenere il controllo senza ridurre affidabilità e developer experience.

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:

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.

Regola pratica: se un workload usa stabilmente meno del 20-30% della request dichiarata, probabilmente stai pagando capacità prenotata che non produce valore. Se invece è spesso vicino alla request o al limite, il problema può essere opposto: rischio di throttling o OOM.

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 è:

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:

Cattivi candidati:

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:

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:

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:

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

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.


FG
Fabrizio Galiano
Founder & SRE — Xseven SRLS

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