Lesson  in  Kubernetes per Ingegneri — Dal Container al Cluster

Workload, Configurazione e Storage

Deployment → ReplicaSet → Pod, rolling update e rollback. ConfigMap e Secret. StatefulSet con PVC, DaemonSet e Job: scegliere il tipo corretto per ogni workload.

Playground per questo incontro: usa il playground Kubernetes multi-nodo su iximiuz Labs: https://labs.iximiuz.com/playgrounds/k8s-omni


Obiettivi dell'incontro

Al termine di questo incontro i partecipanti saranno in grado di:

  • Spiegare la gerarchia Deployment → ReplicaSet → Pod e il meccanismo di rolling update
  • Configurare rolling update con maxSurge e maxUnavailable per zero downtime
  • Configurare un'applicazione con ConfigMap e Secret (volume e variabile d'ambiente)
  • Creare un StatefulSet con Headless Service e PersistentVolumeClaim e verificare la persistenza
  • Scegliere il tipo di workload corretto tra Deployment, DaemonSet, StatefulSet, Job, CronJob
  • Capire QoS classes e il loro impatto sulla gestione della pressione di risorse

Teoria (50 min)

La Gerarchia: Deployment → ReplicaSet → Pod

Deployment        (strategia di update: RollingUpdate o Recreate)
  └── ReplicaSet-v1  (vecchia versione, scalata a 0 dopo update)
        └── Pod (old)
        └── Pod (old)
  └── ReplicaSet-v2  (nuova versione, mantenuta come rollback)
        └── Pod (new)
        └── Pod (new)
        └── Pod (new)

Il Deployment controller gestisce la progressione:

  1. Crea una nuova ReplicaSet per ogni update (kubectl set image, kubectl apply)
  2. Scala la nuova ReplicaSet su e la vecchia giù, rispettando maxSurge/maxUnavailable
  3. Mantiene la storia delle ReplicaSet (configurabile con revisionHistoryLimit)
# Vedi la storia del Deployment
kubectl rollout history deployment/myapp

# Dettagli di una specifica revision
kubectl rollout history deployment/myapp --revision=3

# Rollback all'ultima revision stabile
kubectl rollout undo deployment/myapp

# Rollback a una revision specifica
kubectl rollout undo deployment/myapp --to-revision=2

# Monitora il rolling update in corso
kubectl rollout status deployment/myapp

# Pausa un rollout in corso (per A/B testing manuale)
kubectl rollout pause deployment/myapp
kubectl rollout resume deployment/myapp

Rolling Update: maxSurge e maxUnavailable

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 6
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # Max pod in PIÙ rispetto a replicas durante l'update
      maxUnavailable: 1  # Max pod NON READY durante l'update
  # → aggiorna 2 alla volta: +2 nuovi, -1 vecchi, finché tutti sono aggiornati
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:v2
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          # CRITICO: senza readiness probe, kubernetes non aspetta che il nuovo
          # pod sia pronto prima di eliminare il vecchio → possibile downtime

minReadySeconds — prevenire i falsi "ready":

Per zero-downtime reale, aggiungi spec.minReadySeconds al Deployment. Senza questo campo, Kubernetes conta un Pod come "ready" non appena la readiness probe risponde una volta — anche se il container va subito in crash.

spec:
  minReadySeconds: 10   # Il Pod deve essere Ready CONTINUAMENTE per 10s prima che il rollout avanzi
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Avviso: maxUnavailable: 0 e maxSurge: 0 contemporaneamente non sono validi — l'apiserver rifiuta la configurazione. Almeno uno dei due deve essere > 0.

Strategia Recreate — utile per applicazioni che non supportano più versioni simultanee:

strategy:
  type: Recreate
  # Elimina TUTTI i Pod vecchi prima di crearne di nuovi → downtime garantito

Tipi di Workload: Quale Scegliere

TipoGaranzieUsato per
DeploymentN repliche intercambiabili, rolling updateAPI server, frontend, worker stateless
DaemonSetUn Pod per ogni nodo (o subset)log collector, monitoring agent, CNI plugin
StatefulSetIdentità stabile, storage dedicato, ordine di avvioPostgreSQL, Kafka, Zookeeper, Redis cluster
JobCompletamento garantito (non loop eterno)Migrazione DB, report generation, batch
CronJobEsecuzione periodicaBackup notturno, cleanup, report schedulati
# DaemonSet — garantisce un Pod su ogni nodo
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
spec:
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule  # Schedula anche sul control plane
      containers:
      - name: fluentbit
        image: fluent/fluent-bit:2.2
        volumeMounts:
        - name: varlog
          mountPath: /var/log
          readOnly: true
      volumes:
      - name: varlog
        hostPath:
          path: /var/log

---
# Job — completamento one-shot
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 3    # Riprova max 3 volte
  template:
    spec:
      restartPolicy: OnFailure  # Job devono essere OnFailure o Never (non Always)
      containers:
      - name: migrate
        image: myapp-migrations:1.5.0
        envFrom:
        - secretRef:
            name: db-credentials

---
# CronJob — esecuzione periodica
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"     # Ogni notte alle 2:00 UTC
  concurrencyPolicy: Forbid  # Non avviare se il precedente è ancora in corso
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: myapp-backup:latest

Sintassi del campo schedule (formato cron):

┌───────── minuto (0-59)
│ ┌─────── ora (0-23)
│ │ ┌───── giorno del mese (1-31)
│ │ │ ┌─── mese (1-12)
│ │ │ │ ┌─ giorno della settimana (0-6, 0=domenica)
│ │ │ │ │
* * * * *
ScheduleSignificato
0 2 * * *Ogni notte alle 2:00
*/15 * * * *Ogni 15 minuti
0 9 * * 1-5Alle 9:00, lunedì-venerdì
0 0 1 * *Primo giorno di ogni mese a mezzanotte

Tip: usa crontab.guru per verificare e testare le espressioni cron.

Comportamento degli Init Container in caso di fallimento:

Se un init container fallisce, il Pod entra in CrashLoopBackOff senza mai avviare i container principali. Il kubelet riprova l'init container con backoff esponenziale (10s, 20s, 40s... fino a 5 min).

# Diagnostica: vedi quale init container fallisce
kubectl describe pod <pod> | grep -A 10 "Init Containers:"
kubectl logs <pod> -c <init-container-name>

PodDisruptionBudget (PDB)

Un PDB limita il numero di Pod che possono essere interrotti contemporaneamente durante disruption volontarie (drain, upgrade, scale-down) — non protegge da crash involontari.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2        # almeno 2 Pod devono restare attivi
  # oppure:
  # maxUnavailable: 1    # al massimo 1 Pod può essere non disponibile
  selector:
    matchLabels:
      app: myapp
# Crea il PDB
kubectl apply -f - <<'EOF'
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: nginx
EOF

# Verifica lo stato del PDB
kubectl get pdb
kubectl describe pdb nginx-pdb

# Prova a fare drain di un nodo — il PDB blocca se non ci sono abbastanza repliche
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data

Quando usare un PDB:

ScenarioPDB consigliato
API server con 3 replicheminAvailable: 2
Database StatefulSet (1 replica)maxUnavailable: 0 (blocca il drain)
Worker pool grande (10 repliche)maxUnavailable: 25%
Batch JobNon serve PDB

Buona pratica: ogni Deployment in produzione con ≥2 repliche dovrebbe avere un PDB. Senza PDB, kubectl drain può terminare tutti i Pod contemporaneamente.


Probes: Startup, Readiness, Liveness

Il kubelet esegue tre probe indipendenti su ogni container per capire stato interno vs stato presunto dal processo in esecuzione. Senza probe, Kubernetes considera un container "ok" se il processo PID 1 non esce — ma un'app può essere bloccata, in deadlock o ancora in warm-up.

ProbeDomanda a cui rispondeConseguenza fallimentoDefault se omessa
startupProbe"L'app ha finito il bootstrap?"kubelet disabilita le altre probe finché non passa, poi al fallimento riavvia il containerNon attiva
readinessProbe"L'app è pronta a ricevere traffico?"Il Pod viene rimosso dagli Endpoints del Service — il Pod resta in esecuzioneIl Pod è Ready appena il container parte
livenessProbe"L'app è bloccata e va riavviata?"kubelet uccide il container (restart policy: Always → riavvio)Il Pod è Alive finché il processo non termina

Tutti e tre i tipi supportano gli stessi meccanismi di check:

# HTTP GET — più comune per web service
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
    httpHeaders:
    - name: X-Probe
      value: kubelet

# TCP socket — per servizi non-HTTP (database, cache)
readinessProbe:
  tcpSocket:
    port: 5432

# Exec — esegue un comando nel container (exit 0 = successo)
readinessProbe:
  exec:
    command: ["pg_isready", "-U", "postgres"]

# gRPC — richiede che l'app implementi il grpc health protocol
livenessProbe:
  grpc:
    port: 9000
    service: myservice   # opzionale: nome del servizio gRPC da testare

Parametri temporali (validi per tutti e tre i tipi):

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  initialDelaySeconds: 10   # Attesa PRIMA del primo check (dopo la startupProbe, se presente)
  periodSeconds: 10         # Intervallo tra i check
  timeoutSeconds: 2         # Timeout singolo check
  failureThreshold: 3       # N fallimenti consecutivi → azione (restart/remove)
  successThreshold: 1       # N successi consecutivi per tornare healthy (liveness: deve essere 1)

Esempio completo: i tre tipi insieme

containers:
- name: slow-jvm-app
  image: myapp:v1
  ports:
  - containerPort: 8080
  # Startup: dà tempo all'app di avviarsi (fino a 5 min: 30 x 10s)
  # Durante questo periodo, readiness e liveness NON vengono eseguite.
  startupProbe:
    httpGet: { path: /healthz, port: 8080 }
    failureThreshold: 30
    periodSeconds: 10
  # Readiness: controlla se l'app può ricevere traffico.
  # Fallimento → il Pod esce dal Service, ma non viene riavviato.
  readinessProbe:
    httpGet: { path: /ready, port: 8080 }
    periodSeconds: 5
    failureThreshold: 3
  # Liveness: ultima risorsa — uccide il container se bloccato.
  # Endpoint separato: /ready può dipendere dal DB, /healthz solo dal processo.
  livenessProbe:
    httpGet: { path: /healthz, port: 8080 }
    periodSeconds: 20
    failureThreshold: 3
    initialDelaySeconds: 0   # Non serve: la startupProbe già ha dato tempo

Readiness vs Liveness — endpoint separati:

EndpointCosa verificaDipendenze
/ready (readinessProbe)"Posso servire richieste?"DB raggiungibile, cache inizializzata, config caricata
/healthz (livenessProbe)"Il processo è vivo?"Solo lo stato interno — NON dipendenze esterne

Antipattern critico: usare lo stesso endpoint (es. /health che controlla il DB) per liveness e readiness. Se il DB va giù, tutti i Pod vengono uccisi simultaneamente dalla livenessProbe, mentre il problema è solo downstream. Il restart peggiora la situazione: nuovo Pod → nuova connessione DB → stesso errore → CrashLoopBackOff. La readinessProbe li avrebbe solo tolti dal Service, lasciando spazio al recovery.

Antipattern comuni

SintomoCausa probabile
Pod in CrashLoopBackOff poco dopo lo startlivenessProbe senza startupProbe + initialDelaySeconds troppo basso — kubelet uccide il container prima che finisca il bootstrap
Service senza endpoint durante il rolling updateNessuna readinessProbe → Pod "Ready" appena parte il processo, ma non può ancora rispondere
Cluster instabile quando il DB è lentolivenessProbe che controlla dipendenze esterne → restart a cascata
Restart continui per un'app Java/Ruby pesantelivenessProbe senza startupProbe — il warm-up (JIT, preload) sfora l'initialDelaySeconds

Regola generale: ogni container di produzione deve avere almeno una readinessProbe. Aggiungi startupProbe se il bootstrap supera i ~30 secondi. Aggiungi livenessProbe solo se hai un endpoint che verifica genuinamente che il processo sia bloccato (es. deadlock detection, coda event loop piena) — e mai con check di dipendenze esterne.


ConfigMap e Secret: Configurazione come Oggetti Kubernetes

ConfigMap — configurazione non sensibile (env vars, file di config, script):

# Da literal
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=debug \
  --from-literal=APP_ENV=production

# Da file (il nome del file diventa la chiave)
kubectl create configmap nginx-config --from-file=nginx.conf

# Da directory (tutti i file diventano chiavi)
kubectl create configmap app-configs --from-file=./config-dir/

# Via YAML
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "debug"
  DATABASE_HOST: "postgres.production.svc.cluster.local"
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://backend:8080;
      }
    }
EOF

Secret — dati sensibili (password, token, certificati):

# Da literal (base64-encoded automaticamente)
kubectl create secret generic db-credentials \
  --from-literal=DB_USER=admin \
  --from-literal=DB_PASSWORD=supersecret

# Secret TLS
kubectl create secret tls myapp-tls \
  --cert=tls.crt --key=tls.key

# Secret per registry privato
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypassword

Attenzione: i Secret in Kubernetes sono base64-encoded, NON encrypted, per default. Per encryption at rest, configurare l'EncryptionConfiguration nell'apiserver. Per gestione avanzata, usare External Secrets Operator + HashiCorp Vault / AWS Secrets Manager.

# Vedi il valore di un secret (decodificato)
kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

Mounting: File vs Variabile d'Ambiente

spec:
  containers:
  - name: app
    image: myapp:v1

    # ── VARIABILI D'AMBIENTE ──────────────────────────────────────────────
    # Singola chiave come env var
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: DB_PASSWORD
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: LOG_LEVEL

    # Tutte le chiavi del ConfigMap/Secret come env vars (nome chiave = nome var)
    envFrom:
    - configMapRef:
        name: app-config
    - secretRef:
        name: db-credentials

    # ── VOLUME MOUNT ──────────────────────────────────────────────────────
    # Il file di configurazione appare come file sul filesystem
    # VANTAGGIO: aggiornabile senza riavviare il Pod (hot reload se l'app supporta SIGHUP)
    # TEMPISTICA: il kubelet sincronizza la ConfigMap ogni ~1 minuto (--sync-frequency default).
    # La modifica diventa visibile nel volume in 60-90 secondi dall'aggiornamento della ConfigMap.
    # Le variabili d'ambiente (env/envFrom) NON si aggiornano — richiedono riavvio del Pod.
    volumeMounts:
    - name: nginx-config
      mountPath: /etc/nginx/conf.d/
      readOnly: true
    - name: tls-certs
      mountPath: /etc/ssl/private/
      readOnly: true

  volumes:
  - name: nginx-config
    configMap:
      name: nginx-config        # Ogni chiave → un file
      defaultMode: 0444         # read-only
  - name: tls-certs
    secret:
      secretName: myapp-tls
      defaultMode: 0400         # owner read-only (certificati privati)

Quando usare volume vs env var:

CriterioVolumeEnv Var
File di configurazione
Certificati/chiavi private
Aggiornamento hot reload
Password/token brevi
SIGTERM sensibileDipende

PersistentVolume, PVC e StorageClass

StorageClass (definisce il "tipo" di storage: SSD, HDD, network, local)
    │
    ▼ (provisioning dinamico)
PersistentVolume (PV) — il volume fisico (creato dal cloud provider o staticamente)
    │
    ▼ (binding 1:1)
PersistentVolumeClaim (PVC) — la "richiesta" dello sviluppatore
    │
    ▼ (montato come volume)
Pod → container

StorageClass — definisce come viene provisionato il volume:

# StorageClass per EBS su AWS
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
reclaimPolicy: Delete    # Elimina il PV quando il PVC viene cancellato
volumeBindingMode: WaitForFirstConsumer  # Aspetta che il Pod sia schedulato
allowVolumeExpansion: true

PVC — la richiesta dello sviluppatore:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
spec:
  accessModes:
    - ReadWriteOnce    # Un solo nodo può montare in R/W
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 50Gi

Access Modes:

ModeAbbreviazioneSignificato
ReadWriteOnceRWOMontabile in R/W da un solo nodo (single-node databases)
ReadOnlyManyROXMontabile in sola lettura da più nodi (contenuto statico)
ReadWriteManyRWXMontabile in R/W da più nodi (NFS, CephFS)
ReadWriteOncePodRWOPMontabile in R/W da un solo Pod (garantisce unicità)

EmptyDir: Volume Temporaneo del Pod

Un emptyDir è un volume creato quando il Pod viene schedulato e eliminato quando il Pod termina. Tutti i container del Pod possono leggerlo e scriverlo.

volumes:
- name: cache
  emptyDir:
    sizeLimit: 500Mi      # Limite opzionale (evita di riempire il disco del nodo)
- name: ram-cache
  emptyDir:
    medium: Memory         # Backed by RAM (tmpfs) — molto veloce, conta nel memory limit
Tipo di volumeCiclo di vitaPersistenzaCaso d'uso
emptyDirLegato al PodNo — perso con il PodCache, file temporanei, comunicazione tra container
emptyDir (Memory)Legato al PodNo — perso con il PodCache ad alte prestazioni, dati sensibili
hostPathLegato al NodoSì (sul nodo)Log del nodo, socket Docker — evitare in produzione
PVCIndipendenteDatabase, storage persistente

Attenzione: emptyDir con medium: Memory consuma RAM dal memory limit del container. Se il container supera il limit, viene OOM-killed.


ResourceQuota e LimitRange: Governance del Namespace

ResourceQuota — limita le risorse totali consumabili in un namespace:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-a-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "10"          # Max 10 CPU totali richieste
    requests.memory: 20Gi       # Max 20Gi RAM totali richieste
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "50"                  # Max 50 Pod nel namespace
    persistentvolumeclaims: "10"
    services.loadbalancers: "2"

LimitRange — imposta default e limiti per singolo container/Pod:

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-a
spec:
  limits:
  - type: Container
    default:              # Limiti applicati se il container non li specifica
      cpu: 500m
      memory: 256Mi
    defaultRequest:       # Request applicate se il container non le specifica
      cpu: 100m
      memory: 128Mi
    min:                  # Minimo consentito
      cpu: 50m
      memory: 64Mi
    max:                  # Massimo consentito
      cpu: "2"
      memory: 2Gi
# Verifica le quote del namespace
kubectl describe resourcequota -n team-a
# → Used: requests.cpu=3, requests.memory=8Gi
# → Hard: requests.cpu=10, requests.memory=20Gi

# Verifica i limiti per container
kubectl describe limitrange -n team-a

Best practice: in cluster multi-tenant, ogni namespace di team dovrebbe avere sia ResourceQuota (cap totale) sia LimitRange (default per container). Senza LimitRange, i Pod senza requests/limits consumano risorse senza controllo.


StatefulSet: Identità Stabili e Storage Dedicato

Lo StatefulSet garantisce tre cose che il Deployment non può garantire:

  1. Nomi stabili: postgres-0, postgres-1, postgres-2 (non hash casuali)
  2. Ordine di avvio/spegnimento: 0 prima, poi 1, poi 2 (configurabile)
  3. Storage dedicato: ogni Pod ha il suo PVC che sopravvive al riavvio
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None    # Headless Service — crea DNS per ogni Pod individualmente
  selector:
    app: postgres
  ports:
  - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres    # Riferimento al Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: pg-secret
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
        readinessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "1000m"
  # volumeClaimTemplates: genera un PVC per ogni replica
  # → data-postgres-0, data-postgres-1, data-postgres-2
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 20Gi

DNS del StatefulSet con Headless Service:

postgres-0.postgres.default.svc.cluster.local  → IP di postgres-0
postgres-1.postgres.default.svc.cluster.local  → IP di postgres-1
postgres-2.postgres.default.svc.cluster.local  → IP di postgres-2
postgres.default.svc.cluster.local             → tutti i Pod (round-robin)

Update strategy dello StatefulSet:

Per default lo StatefulSet usa RollingUpdate (aggiorna un Pod alla volta, dal più alto al più basso). Per database in produzione, considera OnDelete: il Pod viene aggiornato solo quando lo elimini manualmente, dandoti controllo completo sulla procedura.

spec:
  updateStrategy:
    type: OnDelete   # Aggiorna solo quando il Pod viene eliminato manualmente
    # type: RollingUpdate  # Default — aggiorna automaticamente dal Pod N a 0
# Ordine di avvio: 0 prima, 1 poi, 2 infine
kubectl get pods -l app=postgres -w

# Lo spegnimento è inverso: 2 prima, poi 1, poi 0
kubectl scale statefulset postgres --replicas=0

# I PVC sopravvivono alla cancellazione del Pod (ma NON del StatefulSet!)
kubectl delete pod postgres-0
kubectl get pvc  # data-postgres-0 esiste ancora

# Espandi un PVC (se StorageClass ha allowVolumeExpansion: true)
kubectl patch pvc data-postgres-0 -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'

HPA — Horizontal Pod Autoscaler

HPA scala automaticamente il numero di repliche di un Deployment/StatefulSet basandosi su metriche.

Come Funziona

desiredReplicas = ceil( currentReplicas × (currentMetricValue / desiredMetricValue) )

Esempio:
- Deployment con 3 repliche, target CPU 60%
- CPU media attuale: 90%
- desiredReplicas = ceil(3 × (90 / 60)) = ceil(4.5) = 5
→ HPA scala da 3 a 5 repliche

Il controller valuta le metriche ogni 15 secondi.

REQUISITO: i container devono avere resources.requests configurato, altrimenti HPA non può calcolare la percentuale di utilizzo.

Configurazione (autoscaling/v2)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 20
  metrics:
  # Scala su CPU (metrica più comune)
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60   # Scala quando CPU media > 60%
  # Scala su memoria
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 200Mi
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30    # Aspetta 30s prima di scalare su
      policies:
      - type: Percent
        value: 100                       # Può raddoppiare in una volta
        periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300   # Aspetta 5 min prima di scalare giù
      policies:
      - type: Pods
        value: 1                         # Rimuove al max 1 Pod alla volta
        periodSeconds: 60

Tipi di Metriche

TipoSorgenteEsempio
Resourcemetrics-serverCPU, memoria
PodsCustom metrics adapterreq/sec, connessioni
ExternalExternal metrics adaptermessaggi in coda cloud

Comandi Utili

# Crea un HPA velocemente (autoscaling/v2)
kubectl autoscale deployment myapp --min=2 --max=20 --cpu-percent=60

# Stato dell'HPA
kubectl get hpa
# → myapp-hpa   Deployment/myapp   35%/60%   2   20   3   5m

# Dettagli (mostra condizioni e eventi)
kubectl describe hpa myapp-hpa

# Simula carico per testare
kubectl run loadgen --image=busybox --rm -it -- sh -c \
  'while true; do wget -q -O- http://myapp.production; done'

Problemi Comuni

  • <unknown>/60% → metrics-server non installato o Pod appena creato
  • missing request for cpu → container senza resources.requests
  • Flapping (scale up/down continuo) → aumenta stabilizationWindowSeconds
  • Non scala a zero → HPA nativo ha minReplicas ≥ 1; per scale-to-zero usa KEDA

Approfondimento: l'Incontro 8 (GitOps, Helm) include un esercizio pratico HPA con generatore di carico, metriche custom con Prometheus Adapter, KEDA per event-driven autoscaling, e il confronto HPA vs VPA.


Hands-on Guidato (90 min)

Esercizio 1 — Rolling Update con Zero Downtime

# Crea il Deployment iniziale
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0    # Zero downtime garantito
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: app
        image: nginx:1.24
        resources:
          requests:
            memory: "32Mi"
            cpu: "50m"
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 2
          periodSeconds: 2
EOF

# Avvia un generatore di traffico per verificare zero downtime
kubectl run traffic-gen --image=busybox --restart=Never -- \
  sh -c 'i=0; while true; do
    code=$(wget -qO- -S http://webapp 2>&1 | grep "HTTP/" | awk '"'"'{print $2}'"'"')
    echo "req $i: HTTP $code"
    i=$((i+1))
    sleep 0.3
  done'

# In un altro terminale: esegui il rolling update
kubectl set image deployment/webapp app=nginx:1.25
kubectl rollout status deployment/webapp

# Verifica la history
kubectl rollout history deployment/webapp

# Rollback se necessario
kubectl rollout undo deployment/webapp

# Cleanup
kubectl delete pod traffic-gen

Esercizio 2 — ConfigMap e Secret: Volume e Env Var

# Crea una config nginx personalizzata
cat > nginx.conf <<'EOF'
server {
    listen 80;
    server_name _;
    location / {
        return 200 "Hello from custom nginx config!\n";
    }
    location /health {
        return 200 "OK\n";
    }
}
EOF

kubectl create configmap nginx-custom --from-file=default.conf=nginx.conf

# Crea un secret con "credenziali"
kubectl create secret generic app-secret \
  --from-literal=API_KEY=sk-demo-12345 \
  --from-literal=DB_URL=postgres://admin:pass@db:5432/mydb

# Crea il Pod che usa entrambi
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: configdemo
spec:
  containers:
  - name: nginx
    image: nginx:alpine
    envFrom:
    - secretRef:
        name: app-secret
    volumeMounts:
    - name: nginx-conf
      mountPath: /etc/nginx/conf.d/
  volumes:
  - name: nginx-conf
    configMap:
      name: nginx-custom
EOF

# Verifica il config file montato
kubectl exec configdemo -- cat /etc/nginx/conf.d/default.conf

# Verifica le variabili d'ambiente (secret)
kubectl exec configdemo -- env | grep -E "API_KEY|DB_URL"

# Aggiorna il ConfigMap (hot reload senza riavvio)
kubectl create configmap nginx-custom \
  --from-literal=default.conf="server { listen 80; location / { return 200 'Updated!'; } }" \
  --dry-run=client -o yaml | kubectl apply -f -

# Attendi che Kubernetes sincronizzi (default: ~60s)
sleep 65
kubectl exec configdemo -- cat /etc/nginx/conf.d/default.conf

Esercizio 3 — StatefulSet PostgreSQL con PVC

# Crea il secret per la password
kubectl create secret generic pg-secret --from-literal=password=mypassword

# Applica StatefulSet + Headless Service
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
  - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16-alpine
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: pg-secret
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
        readinessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 10
          periodSeconds: 5
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
EOF

# Aspetta che sia Ready
kubectl wait pod/postgres-0 --for=condition=ready --timeout=120s

# Crea dati di test
kubectl exec -it postgres-0 -- psql -U postgres <<'SQL'
CREATE TABLE test_data (id SERIAL, value TEXT);
INSERT INTO test_data (value) VALUES ('data persists after restart!');
SELECT * FROM test_data;
SQL

# Verifica i PVC creati
kubectl get pvc
# → data-postgres-0   Bound   1Gi   RWO

# Cancella il Pod (simula crash) — il PVC sopravvive
kubectl delete pod postgres-0
kubectl wait pod/postgres-0 --for=condition=ready --timeout=120s

# Verifica che i dati persistano
kubectl exec -it postgres-0 -- psql -U postgres -c "SELECT * FROM test_data;"

Esercizio 4 — CronJob per Backup

kubectl apply -f - <<'EOF'
apiVersion: batch/v1
kind: CronJob
metadata:
  name: log-cleanup
spec:
  schedule: "*/1 * * * *"    # Ogni minuto (per demo; in prod ogni notte)
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: cleanup
            image: busybox:1.36
            command: ['sh', '-c', 'echo "Cleanup at $(date): removing logs older than 30d"; ls /tmp']
EOF

# Aspetta che il primo Job venga triggerato (~1 minuto)
kubectl get cronjob log-cleanup -w

# Vedi i Job creati
kubectl get jobs
kubectl logs job/<job-name>

Capstone Challenge (30 min)

"Il Database che Dimentica"

Viene fornita un'applicazione con un Deployment PostgreSQL che perde tutti i dati ad ogni restart. Il tuo compito:

  1. Converti il Deployment in StatefulSet con Headless Service
  2. Aggiungi un PVC per /var/lib/postgresql/data (1Gi, ReadWriteOnce)
  3. Sposta la password in un Secret (non hardcodata in env.value)
  4. Aggiungi un initContainer che popola una tabella di test al primo avvio
  5. Aggiungi una readinessProbe con pg_isready
  6. Verifica la persistenza: kubectl delete pod postgres-0 → i dati ci sono ancora
# Verifica finale
kubectl exec postgres-0 -- psql -U postgres -c "SELECT count(*) FROM test_data;"
# → count = 1 (dato sopravvissuto al restart)

Bonus: aggiungi un CronJob che inserisce un record ogni 5 minuti come "heartbeat".


Self-Study Assignment

Completa le challenge su iximiuz Labs prima del prossimo incontro (60–90 min totali). Cerca nella sezione Challenges della piattaforma:

  • "Configure a Pod with a ConfigMap" — ConfigMap come volume
  • "Store Kubernetes Secrets" — Secret management
  • "Kubernetes Deployment with Rolling Update" — rolling update estrategia
  • "Kubernetes StatefulSet with Persistent Volume" — storage persistente
  • "QoS Classes" — resource constraints

Letture consigliate:


Risorse Aggiuntive

Documentazione Ufficiale Kubernetes

Gestione Avanzata dei Segreti

Storage: Driver CSI per i Cloud Provider

  • AWS EBS CSI Driver — volumi EBS dinamici su EKS: gp3, io1, io2 con encryption e snapshots
  • GCP Persistent Disk CSI Driver — volumi GCE Persistent Disk su GKE: pd-standard, pd-ssd, pd-balanced
  • Azure Disk CSI Driver — Azure Managed Disk su AKS: Standard_LRS, Premium_LRS, UltraSSD_LRS
  • Rook Ceph — storage orchestrator per Ceph in Kubernetes: block, filesystem, object storage su bare-metal
  • Longhorn — CNCF — storage distribuito per Kubernetes: replica, backup su S3, snapshot, UI web

Backup e Disaster Recovery

  • Velero — VMware — backup e restore di risorse Kubernetes e PersistentVolumes su S3/GCS/Azure Blob
  • Kasten K10 — Veeam — piattaforma enterprise di backup e mobility per Kubernetes con policy-based automation
  • etcdadm — gestione del lifecycle di etcd: backup, restore, cluster upgrade

Blog e Tutorial

Strumenti di Debug Workload

  • kubectl debug — debug di Pod in esecuzione con container effimeri (ephemeral containers)
  • Komodor — Kubernetes troubleshooting platform con timeline degli eventi e root cause analysis
  • Robusta — alerting basato su Prometheus con automazione della risposta agli incidenti K8s