Usa build multi-stage per mantenere piccole le immagini di runtime
Compila e assembla in uno stage di build che dispone di SDK e strumenti, poi copia solo gli artefatti risultanti in uno stage finale minimale, così che compilatori e dipendenze di build non finiscano mai in produzione. Immagini più piccole si scaricano più in fretta, si avviano più rapidamente e presentano una superficie d'attacco minore.
- Fai: un primo stage
FROM golang:1.22 AS buildche produce un binario, poiFROM gcr.io/distroless/staticconCOPY --from=build /app/server /server. - Non fare: spedire l'intera immagine SDK con sorgente, cache dei pacchetti e strumenti di build cotti dentro il container in esecuzione.
Esegui i container come utente non-root
Un processo che gira come root dentro il container è root rispetto al kernel, quindi una fuga dal container diventa una compromissione dell'host. Crea e passa a un utente non privilegiato nell'immagine, e imponilo in Kubernetes così che un'immagine mal costruita non possa girare con privilegi.
- Fai:
USER 10001nel Dockerfile esecurityContext: { runAsNonRoot: true }sul Pod. - Non fare: lasciare l'utente root predefinito e montare il socket Docker o concedere
privileged: true"per farlo funzionare".
Fissa le immagini per digest, non per un tag mutabile
Tag come latest o 1 possono essere ripuntati a contenuti diversi nel tempo, quindi un deploy che "funzionava ieri" potrebbe scaricare un'immagine diversa oggi, rompendo riproducibilità e integrità della supply chain. Fissa il digest SHA-256 immutabile per tutto ciò che rilasci.
- Fai:
image: registry/app@sha256:...nei manifest di produzione. - Non fare: affidarti a
image: app:latestdando per scontato che i bit in esecuzione non cambino mai.
Ordina i layer del Dockerfile dal meno al più frequentemente modificato
Ogni istruzione è un layer in cache, e una modifica invalida quel layer e tutto ciò che segue. Installa le dipendenze (che cambiano di rado) prima di copiare il sorgente applicativo (che cambia di continuo) così che le ricostruzioni riusino i layer delle dipendenze.
- Fai:
COPY package.json package-lock.json ./poiRUN npm ci, e solo dopoCOPY . .. - Non fare:
COPY . .per primo, che invalida la cache di installazione delle dipendenze a ogni modifica del sorgente.
Imposta sia le richieste sia i limiti di risorse
Le richieste permettono allo scheduler di piazzare i Pod sensatamente e ne definiscono la Quality of Service; i limiti impediscono a un singolo container di affamare i vicini. Senza richieste, i Pod finiscono su nodi in overcommit e sono sfrattati per primi; senza limiti, una perdita può mettere giù un nodo.
- Fai: imposta richiesta di memoria = limite per carichi prevedibili di classe Guaranteed, e una richiesta di CPU con un limite sensato.
- Non fare: ometterle entrambe e sperare, per poi fare debug di outage da vicini rumorosi e OOM kill a sorpresa.
Definisci correttamente le probe di liveness e readiness
La readiness regola il traffico così che le richieste non colpiscano mai un Pod che si sta ancora avviando o temporaneamente sovraccarico; la liveness riavvia un container davvero bloccato. Confonderle, o rendere la liveness troppo aggressiva, causa cicli di riavvio inutili che sembrano instabilità.
- Fai: una probe di readiness economica su un endpoint
/readyze una probe di liveness più conservativa su/healthz. - Non fare: puntare una probe di liveness stringente su un percorso di avvio lento così che Pod sani vengano uccisi a metà boot.
Non archiviare mai segreti in immagini, ConfigMap o Git
Le ConfigMap e i layer delle immagini non sono riservati, e i Secret di Kubernetes sono solo codificati in base64 a riposo a meno che non si abiliti la cifratura. Tratta il materiale segreto come sensibile end-to-end e tienilo fuori da artefatti di build e controllo di versione.
- Fai: abilita la cifratura a riposo di etcd, limita i Secret con RBAC e iniettali tramite Secret montati o un secrets operator esterno.
- Non fare: cuocere una chiave API in un layer ENV o committare un manifest Secret con valori reali nel repository.
Applica NetworkPolicy a default-deny
Per impostazione predefinita ogni Pod può raggiungere ogni altro Pod, quindi un carico compromesso può muoversi liberamente. Parti da una policy default-deny per namespace e consenti esplicitamente solo l'ingress e l'egress di cui ogni carico ha bisogno (minimo privilegio).
- Fai: una NetworkPolicy default-deny-all più regole di allow strette dai Pod client specifici, fatte rispettare da una CNI capace (Calico, Cilium).
- Non fare: lasciare il namespace completamente aperto affidandoti al fatto che i Service siano "interni" come se fosse un confine di sicurezza.
Gestisci lo stato con il controller giusto e storage reale
I carichi stateless appartengono ai Deployment; i carichi che necessitano di identità stabile e storage durevole per replica appartengono agli StatefulSet con PersistentVolumeClaim. Scrivere dati importanti nel layer scrivibile effimero di un container garantisce la perdita dei dati al rescheduling.
- Fai: uno StatefulSet con un
volumeClaimTemplateper un database, e una StorageClass con backup. - Non fare: eseguire un database come Deployment scrivendo su storage locale del container e scoprire la perdita dopo che un nodo viene drenato.
Usa un PodDisruptionBudget per la disponibilità durante le disruption
Le disruption volontarie (drain dei nodi, upgrade del cluster, scale-down dell'autoscaler) possono sfrattare più repliche contemporaneamente e causare un outage. Un PodDisruptionBudget limita quante repliche possono essere indisponibili simultaneamente così che la manutenzione resti sicura.
- Fai: imposta
minAvailable: 2(o una percentuale) su un Deployment multi-replica che deve restare in servizio. - Non fare: eseguire due repliche senza budget e far sì che il drain di un singolo nodo le metta giù entrambe.
Rendi i deployment dichiarativi e versionati (GitOps)
Le modifiche imperative con kubectl edit divergono da qualsiasi registrazione e sono difficili da rivedere o sottoporre a rollback. Dichiara lo stato desiderato in Git e lascia che un riconciliatore (Argo CD, Flux) lo applichi e lo faccia rispettare continuamente, trasformando i rollback in revert e le modifiche in pull request rivedibili.
- Fai: archivia i manifest/values Helm in Git e fai riconciliare il cluster live ad essi da un agente in-cluster.
- Non fare: applicare patch a caldo in produzione a mano perdendo la tracciabilità e la capacità di riprodurre lo stato.
Scansiona le immagini e fissa una base minimale in CI
Le vulnerabilità arrivano tramite immagini base e pacchetti transitivi, quindi un'immagine pulita alla build può diventare sfruttabile in seguito. Scansiona a ogni build, ricostruisci agli aggiornamenti dell'immagine base e preferisci basi minimali/distroless che contengono meno pacchetti da aggiornare.
- Fai: esegui uno scanner di immagini (Trivy, Grype) nella pipeline e fai fallire la build su CVE critiche risolvibili.
- Non fare: costruire una volta da una base grassa e non riscansionare o ricostruire mai, accumulando CVE non aggiornate.
Gestisci i segnali di terminazione e imposta grace period sensati
Kubernetes invia SIGTERM e attende terminationGracePeriodSeconds prima di SIGKILL; un processo che ignora SIGTERM perde le richieste in volo a ogni rollout. Intercetta il segnale, smetti di accettare nuovo lavoro, drena ed esci in modo pulito, e usa un hook preStop più il flip della readiness per coordinarti con i bilanciatori.
- Fai: gestisci SIGTERM per terminare le richieste in volo, e usa un breve sleep preStop così che gli endpoint si aggiornino prima che il processo esca.
- Non fare: eseguire l'app come PID 1 in forma shell così che non riceva mai i segnali, per poi vedere 502 a ogni deploy.