Docker non introduce alcuna primitiva di isolamento nuova nel kernel Linux: mette insieme cgroups, namespace e un filesystem union — tutti già nel kernel — dietro un’unica interfaccia a riga di comando e un formato di immagine versionato. Il progetto è stato rilasciato come open source da dotCloud nel marzo 2013, con licenza Apache 2.0, ed è scritto in Go.
Contesto
Il problema che Docker affronta non è eseguire processi in isolamento, cosa risolta da anni, ma distribuirli. Confezionare un’applicazione con tutte le sue dipendenze in modo che si comporti allo stesso modo sul portatile di chi sviluppa, in staging e in produzione è rimasto un lavoro manuale e fragile. Le macchine virtuali isolano, ma replicano un sistema operativo intero per ogni istanza, e lo pagano in tempo di avvio e in memoria.
Le tecnologie su cui Docker poggia sono mature. I cgroups (control group) sono entrati nel kernel mainline con la versione 2.6.24 nel gennaio 2008; il lavoro era partito in Google nel 2006 sotto il nome di process containers. I namespace esistono nel kernel dal 2002 circa — il namespace PID è arrivato nel luglio 2007 — e l’idea viene dall’uso pervasivo dei namespace in Plan 9 di Bell Labs. LXC e i driver di libvirt espongono già queste primitive. Quel che mancava era una catena che andasse dalla descrizione di un’applicazione alla sua esecuzione riproducibile su una macchina qualunque.
Architettura
Docker, nelle versioni di questo periodo, non parla direttamente al kernel: usa LXC come driver di esecuzione predefinito e gli delega la creazione dei namespace e la configurazione dei cgroups. Tre famiglie di primitive lavorano insieme.
I namespace danno a ciascun container la propria vista del sistema: namespace separati per PID, rete, mount, IPC e hostname fanno sì che un processo dentro il container veda solo i propri processi, la propria interfaccia di rete, il proprio albero di filesystem. Non c’è un kernel guest: i processi del container sono processi normali sull’host, con una visibilità ristretta.
I cgroups impongono i limiti di risorsa — CPU, memoria, I/O su disco — e contabilizzano i consumi. Sono lo stesso meccanismo con cui il kernel gestisce le risorse a livello di sistema, applicato qui per gruppo di processi.
Il filesystem union, in questo periodo AUFS, è la parte che separa Docker dall’uso diretto di LXC. Un’immagine è una pila di layer in sola lettura sovrapposti; quando un container parte, sopra si aggiunge un layer scrivibile sottile, con semantica copy-on-write. I layer comuni a più immagini sono condivisi sul disco e nella cache, quindi creare un container non duplica il filesystem di base. Così l’avvio resta intorno al secondo e l’occupazione su disco rimane contenuta quando molti container derivano da una stessa immagine base.
Sopra queste primitive sta il formato che rende il tutto distribuibile. Un Dockerfile descrive in modo dichiarativo come costruire un’immagine: si parte da un’immagine base, e ogni istruzione — installare un pacchetto, copiare un file, impostare una variabile d’ambiente — produce un nuovo layer. L’immagine risultante è immutabile e identificata da un hash. Il comando docker run la scarica se manca in locale, ne deriva un container e lo avvia. Le immagini si scambiano attraverso un registry, un archivio da cui si fa pull e push come con un repository Git.
Il punto critico
La scelta progettuale decisiva è la separazione fra il piano di esecuzione e il piano di distribuzione. L’isolamento via namespace e cgroups era già a portata di mano con LXC, ma LXC non dice nulla su come un’immagine venga costruita, versionata e spostata fra macchine. Docker tiene LXC dietro un’astrazione e aggiunge ciò che mancava: un formato di immagine a layer con copy-on-write e un protocollo di distribuzione. Sono questi due insieme — immagine immutabile più trasporto — a fare del deployment un’operazione deterministica invece di una checklist di configurazione.
Che il driver di esecuzione sia uno strato sostituibile è dichiarato fin da subito. LXC è il driver predefinito di oggi, ma l’interfaccia verso il kernel sta dietro un driver, e niente nel formato dell’immagine dipende da LXC. Si possono quindi raggiungere le primitive del kernel per altre vie, senza toccare immagini né Dockerfile.
Implicazioni
Per chi sviluppa, l’effetto immediato è che la stessa immagine costruita in locale è quella che gira in produzione, byte per byte. Le dipendenze sono esplicite nel Dockerfile e non più nascoste nello stato della macchina. La classe di guasti del tipo “sulla mia macchina funziona” si riduce a una differenza di kernel o di configurazione dell’host, una superficie molto più ridotta di prima.
C’è anche una ricaduta sull’ecosistema open source. Distribuire software come immagine eseguibile, accanto al pacchetto di una distribuzione o al codice da compilare, abbassa il costo per chi vuole provare un progetto: un docker run su un’immagine pubblicata prende il posto di una pagina di istruzioni d’installazione. Il rovescio è che la riproducibilità si sposta dalla ricetta di build all’artefatto binario, e un’immagine opaca è verificabile solo quanto lo è il Dockerfile che l’ha generata.
Limiti
A questa data Docker è agli inizi e va trattato come tale. Il progetto è dichiarato non pronto per la produzione, le interfacce cambiano da una release all’altra e l’unico driver è LXC. La condivisione del kernel fra host e container significa che l’isolamento vale quanto garantiscono namespace e cgroups — più debole della separazione di un hypervisor — con una superficie di attacco che comprende l’intera interfaccia delle system call del kernel; per carichi multi-tenant non fidati questo va valutato in modo esplicito. La dipendenza da AUFS lega il funzionamento a un componente che non sta nel kernel mainline, con quel che ne segue sulla portabilità fra distribuzioni. Sono vincoli da soppesare prima di portare i container fuori dall’ambiente di sviluppo e di test.
- https://www.docker.io/
- https://github.com/dotcloud/docker
- https://pyvideo.org/pycon-us-2013/the-future-of-linux-containers.html
- https://en.wikipedia.org/wiki/Cgroups
- https://wiki.openvz.org/History_of_containers_in_Linux_kernel
- https://www.noze.it/insights/docker-open-source/
Immagine di copertina: Solomon Hykes, fondatore di Docker, parla seduto su una poltrona durante una conferenza tecnologica — foto di Anthony Harvey / Getty Images for TechCrunch, CC BY 2.0 — https://commons.wikimedia.org/wiki/File:518392245AG185_TechCrunch_D_-_Solomon_Hykes.jpg