Container e orchestrazione — Docker e Kubernetes

📋 Obiettivi di apprendimento
Distinguere VM e container spiegando perché i container condividono il kernel dell’host e quali vantaggi e limiti ne derivano
Descrivere il funzionamento di Docker: immagini, layer, Dockerfile, registry e i principali comandi della CLI
Spiegare il networking dei container Docker: bridge, host e overlay network, e come i container comunicano tra loro
Descrivere l’architettura Kubernetes con i concetti di pod, deployment, service e ingress, e il ruolo del control plane
📄
Slides
Data Center — architettura, topologie e Tier

Il problema che i container risolvono

Prima di parlare di container, vale la pena capire il problema che hanno risolto. Immagina uno sviluppatore che crea un’applicazione web sul proprio laptop con Python 3.11 e una libreria specifica. L’applicazione funziona perfettamente in locale. Poi viene deployata sul server di produzione che ha Python 3.8 e versioni diverse delle librerie. L’applicazione si rompe.

Questa situazione era così comune da avere un nome: “Works on my machine” — funziona sulla mia macchina. I container nascono esattamente per eliminare questo problema: l’applicazione viene impacchettata insieme a tutto il suo ambiente di esecuzione — runtime, librerie, file di configurazione — in un’unica unità portabile che si comporta in modo identico ovunque venga eseguita.

VM vs Container — la differenza fondamentale

Macchina Virtuale (VM)
[App A]   [App B]   [App C]
[OS Guest] [OS Guest] [OS Guest]
[Hypervisor]
[OS Host] + [Hardware]

Ogni VM include un OS completo — kernel, librerie di sistema, servizi — virtualizzato dall’hypervisor. Le VM sono fortemente isolate: kernel separati, spazio di memoria separato.

✅ Isolamento completo — kernel separati
✅ Può eseguire OS diversi (Linux su Windows)
❌ Avvio lento (minuti)
❌ Peso elevato: GB per OS guest
❌ Overhead hypervisor su ogni VM
Container ✅ Leggero e portabile
[App A]   [App B]   [App C]
[Libs A]   [Libs B]   [Libs C]
[Container Runtime — Docker]
[OS Host (kernel condiviso)] + [Hardware]

I container condividono il kernel dell’OS host ma hanno il proprio filesystem, librerie e processi isolati. Nessun OS guest — solo le dipendenze dell’applicazione.

✅ Avvio rapidissimo (secondi o meno)
✅ Leggerissimi: MB invece di GB
✅ Densità alta: migliaia su un host
⚠️ Isolamento minore — kernel condiviso
⚠️ Solo Linux su host Linux (o Windows su Windows)
📌 Kernel condiviso — vantaggi e implicazioni di sicurezza

Condividere il kernel è ciò che rende i container così leggeri e veloci — non devono avviare un intero OS. Ma significa anche che una vulnerabilità nel kernel dell’host potrebbe, in teoria, essere sfruttata da un container per “uscire” dal proprio isolamento (container escape). Per questo in ambienti di produzione si usano tecnologie aggiuntive come seccomp (limitazione delle syscall), AppArmor/SELinux (profili di sicurezza) e si evita di eseguire container con privilegi root non necessari.

Docker — il container runtime più diffuso

Docker è la piattaforma che ha reso i container accessibili a tutti, standardizzando la creazione, distribuzione ed esecuzione dei container. Introdotto nel 2013, ha trasformato il modo in cui il software viene sviluppato e deployato.

Immagini Docker — il blueprint del container

Un’immagine Docker è un pacchetto immutabile che contiene tutto il necessario per eseguire un’applicazione: OS base (es. Ubuntu, Alpine), runtime (Python, Node.js, Java), librerie e dipendenze, codice dell’applicazione, configurazione. L’immagine è il template — il container è l’istanza in esecuzione di quell’immagine, esattamente come una classe è il template e l’oggetto è l’istanza in programmazione OOP.

Il sistema a layer — efficienza dello storage

Le immagini Docker sono costruite a strati sovrapposti (layer). Ogni istruzione nel Dockerfile aggiunge un layer immutabile sopra il precedente. Il vantaggio è enorme: i layer identici tra più immagini vengono memorizzati una volta sola sul disco.

Layer di un’immagine Docker — condivisione tra immagini
Immagine app-web (200 MB)
Layer 4: codice app (5 MB)
Layer 3: dipendenze pip (30 MB)
Layer 2: Python 3.11 (50 MB) ← condiviso
Layer 1: Ubuntu 22.04 (115 MB) ← condiviso
Immagine app-api (195 MB)
Layer 4: codice API (3 MB)
Layer 3: dipendenze pip (27 MB) — diverso
Layer 2: Python 3.11 (50 MB) ← condiviso
Layer 1: Ubuntu 22.04 (115 MB) ← condiviso
I layer condivisi (Ubuntu + Python) occupano spazio disco una sola volta. Le due immagini insieme occupano 200 + 195 = 395 MB in teoria, ma solo 200 + 35 = 235 MB effettivi su disco grazie alla condivisione dei layer comuni.

Il Dockerfile — istruzioni per costruire un’immagine

Il Dockerfile è un file di testo che contiene le istruzioni per costruire un’immagine Docker passo per passo. Ogni istruzione crea un nuovo layer.

Esempio Dockerfile — applicazione web Python
# FROM: specifica l'immagine base su cui costruire
FROM python:3.11-slim

# WORKDIR: imposta la directory di lavoro dentro il container
WORKDIR /app

# COPY: copia file dall'host nel container
COPY requirements.txt .

# RUN: esegue un comando durante la BUILD (crea un layer)
RUN pip install --no-cache-dir -r requirements.txt

# Copia il codice dell'applicazione
COPY . .

# EXPOSE: documenta la porta su cui l'app è in ascolto
EXPOSE 8080

# CMD: comando eseguito all'avvio del container (non crea layer)
CMD ["python", "app.py"]

Registry — il repository delle immagini

Un registry Docker è un server che memorizza e distribuisce immagini. Il registry pubblico ufficiale è Docker Hub (hub.docker.com) con milioni di immagini già pronte — Ubuntu, Nginx, MySQL, Node.js, Python e qualsiasi altra cosa immaginabile. Le aziende usano registry privati (Harbor, AWS ECR, Azure Container Registry) per ospitare le proprie immagini senza renderle pubbliche.

Comandi Docker essenziali

Comandi Docker — workflow completo
# Costruire un'immagine dal Dockerfile nella directory corrente
docker build -t mia-app:1.0 .

# Scaricare un'immagine dal registry (Docker Hub)
docker pull nginx:latest

# Avviare un container in background (-d) con port mapping (-p)
# Porta 8080 dell'host → porta 80 del container
docker run -d -p 8080:80 --name web nginx:latest

# Listare i container in esecuzione
docker ps

# Vedere i log del container
docker logs web

# Aprire una shell interattiva dentro un container in esecuzione
docker exec -it web /bin/bash

# Fermare e rimuovere un container
docker stop web && docker rm web

# Pubblicare l'immagine sul registry
docker push mio-account/mia-app:1.0

Networking dei Container Docker

I container necessitano di comunicare tra loro e con il mondo esterno. Docker gestisce questa comunicazione attraverso reti virtuali software — ogni container ottiene un indirizzo IP virtuale e può comunicare con gli altri secondo le regole della rete a cui è connesso.

Modalità di rete Docker
bridge
Default

La modalità predefinita. Docker crea un bridge virtuale (docker0) sull’host. I container ottengono IP nel range 172.17.0.0/16 e possono comunicare tra loro via IP. Per esporre un servizio all’esterno si usa il port mapping (-p host:container). I container sulla stessa rete bridge si trovano tramite IP o nome container.

Es: container web (172.17.0.2) chiama container db (172.17.0.3) sulla porta 5432
host

Il container condivide direttamente lo stack di rete dell’host — nessuna interfaccia virtuale, nessun NAT. Il container usa le stesse porte e IP dell’host. Massime prestazioni di rete (nessun overhead di traduzione), ma nessun isolamento di rete tra container e host.

Es: container Nginx ascolta direttamente sulla porta 80 dell’host senza port mapping
overlay
Multi-host

Crea una rete distribuita su più host Docker. I container su host fisici diversi vedono la stessa rete logica e comunicano direttamente tramite IP, come se fossero sullo stesso host. Usa VXLAN per incapsulare il traffico L2 dentro UDP su una rete IP esistente. È la modalità usata da Docker Swarm e Kubernetes.

Es: container-A su Host1 chiama container-B su Host2 via IP senza sapere che sono su macchine diverse
none

Il container è completamente isolato dalla rete — nessuna interfaccia, nessuna connettività. Usato per container che elaborano dati locali senza necessità di rete (batch processing, generazione di report, strumenti di analisi offline).

Kubernetes — l’orchestratore di container

Docker risolve il problema di eseguire container su un singolo host. Ma quando si gestiscono centinaia o migliaia di container distribuiti su decine di server, serve uno strumento che risponda automaticamente a domande come: dove avvio questo container? Cosa succede se l’host si guasta? Come distribuisco il traffico tra istanze multiple della stessa app? Come aggiorno l’applicazione senza downtime?

Kubernetes (K8s) è il sistema di orchestrazione di container open source sviluppato da Google e donato alla CNCF (Cloud Native Computing Foundation) nel 2014. È diventato lo standard di fatto per il deployment di applicazioni containerizzate in produzione.

📌 L’origine di Kubernetes

Kubernetes nasce da Borg, il sistema interno con cui Google gestisce tutti i propri servizi (Gmail, YouTube, Ricerca) da oltre un decennio. Borg orchestrava centinaia di migliaia di job su cluster enormi. Kubernetes è essenzialmente Borg riscritto in Go e reso open source — porta a chiunque le stesse tecniche operative usate da Google alla scala più grande del mondo.

Architettura Kubernetes — Control Plane e Worker Node

Cluster Kubernetes — componenti principali
CONTROL PLANE (Master)
API Server: punto di ingresso per tutti i comandi — kubectl vi comunica qui
etcd: database key-value distribuito — memorizza lo stato desiderato del cluster
Scheduler: decide su quale nodo deployare ogni pod in base alle risorse disponibili
Controller Manager: verifica continuamente che lo stato reale corrisponda allo stato desiderato
WORKER NODE (N volte)
kubelet: agente sul nodo che riceve istruzioni dal Control Plane e gestisce i pod
kube-proxy: gestisce le regole di rete per il routing del traffico verso i pod
Container Runtime: Docker, containerd o CRI-O — esegue fisicamente i container
Pod: uno o più container che condividono rete e storage — unità base di K8s

I concetti fondamentali di Kubernetes

📦 Pod

L’unità minima deployabile di Kubernetes. Un pod contiene uno o più container strettamente accoppiati che condividono lo stesso indirizzo IP, la stessa rete e lo stesso volume di storage. I container dentro un pod si comunicano via localhost.

Es: pod con container app + container sidecar per logging
🔁 Deployment

Gestisce il ciclo di vita di un set di pod identici (repliche). Definisce quante repliche del pod devono essere sempre attive. Se un pod muore, il Deployment ne ricrea automaticamente uno nuovo. Permette rolling update senza downtime.

Es: Deployment con replicas: 3 → sempre 3 pod del web server attivi
🔀 Service

I pod hanno IP effimeri — cambiano ogni volta che vengono ricreati. Un Service fornisce un IP stabile e un DNS name che rimane costante. Il Service fa da load balancer tra tutti i pod che corrispondono a una label selettore, distribuendo il traffico automaticamente.

Tipi: ClusterIP (interno), NodePort (esposto sul nodo), LoadBalancer (cloud LB esterno)
🚪 Ingress

Gestisce l’accesso HTTP/HTTPS esterno al cluster. Funziona come un reverse proxy L7: instrada le richieste verso i Service appropriati in base all’URL o al dominio. Un solo Ingress con un solo IP pubblico può servire decine di applicazioni diverse tramite routing basato su hostname o path.

Es: api.azienda.it → service-api; www.azienda.it → service-web

Il principio dichiarativo — “desired state”

📌 Kubernetes è dichiarativo, non imperativo

Con Docker si danno comandi imperativi: “avvia questo container”. Con Kubernetes si dichiara lo stato desiderato: “voglio sempre 3 repliche di questa app”. Il Controller Manager monitora continuamente lo stato reale del cluster e agisce automaticamente per portarlo allo stato desiderato. Se un pod muore, ne viene creato uno nuovo. Se un nodo si guasta, i pod vengono spostati su nodi sani. L’operatore definisce cosa vuole, Kubernetes decide come ottenerlo.

# Esempio YAML di Deployment — stato desiderato dichiarato apiVersion: apps/v1 kind: Deployment metadata: name: web-app spec: replicas: 3 # voglio sempre 3 pod attivi selector: matchLabels: app: web-app template: spec: containers: – name: web image: mia-app:1.0 ports: – containerPort: 8080

IaaS vs CaaS vs FaaS — tre livelli di astrazione

AspettoIaaS
VM nel cloud
CaaS
Container as a Service
FaaS
Function as a Service
Unità deployataMacchina virtualeContainer / PodFunzione / snippet di codice
Gestione OSA carico dell’utenteRuntime gestitoCompletamente astratto
AvvioMinutiSecondiMillisecondi
ScalabilitàManuale o auto-scaling VMHPA KubernetesAutomatica — 0 a ∞
FatturazionePer ora di VM accesaPer risorse containerPer esecuzione singola
EsempiAWS EC2, Azure VMGKE, EKS, AKSAWS Lambda, Azure Functions
📌 FaaS (Serverless) — quando non c’è server da gestire

Con FaaS (es. AWS Lambda) lo sviluppatore scrive solo il codice della funzione — nessun server, nessun container, nessun OS da configurare. La funzione viene eseguita in risposta a un evento (una richiesta HTTP, un messaggio in coda, un file caricato su S3) e viene fatturata al millisecondo di esecuzione. Se non arrivano richieste, la funzione non gira e il costo è zero. Il termine “serverless” è un po’ fuorviante — i server esistono, ma sono completamente nascosti e gestiti dal provider.

📌 Riepilogo — Punti chiave
  • I container condividono il kernel dell’OS host (VM no) — più leggeri (MB vs GB), avvio in secondi, ma isolamento minore. Il problema “works on my machine” viene eliminato impacchettando app + dipendenze in un’immagine portabile
  • Docker: immagini a layer condivisi (risparmio disco), Dockerfile per costruirle, Docker Hub per distribuirle. Bridge (default, isolato), host (prestazioni massime), overlay (multi-host con VXLAN), none (isolamento totale)
  • Kubernetes: Control Plane (API server, etcd, scheduler, controller) + Worker Nodes (kubelet, kube-proxy, container runtime). Principio dichiarativo: si dichiara lo stato desiderato, K8s lo mantiene automaticamente
  • Pod = unità minima (1+ container con IP condiviso); Deployment = gestisce N repliche; Service = IP stabile + load balancing; Ingress = reverse proxy L7 per routing HTTP/HTTPS esterno
  • IaaS: gestisci la VM. CaaS: gestisci il container, il runtime è del provider (GKE, EKS). FaaS/Serverless: scrivi solo la funzione, fatturazione al millisecondo di esecuzione

Lascia un commento