Storia dei contenitori

Negli ultimi anni si è parlato tanto dei contenitori come la soluzione definitiva per porre fine ai problemi di distribuzione delle applicazioni, principalmente grazie al clamore scaturito intorno a Docker, progetto open source diventato ormai sinonimo di container.

Per quanto si abbia l’impressione che si tratti di una tecnologia recente, il concetto di container non è per nulla una novità.

Possiamo vedere i contenitori come una naturale evoluzione dei chroot e delle FreeBSD “jails”, disponibili rispettivamente già dagli anni 80 e 2000. Su Linux, scopriamo che i precursori di Docker erano già disponibili fin dal 2005 grazie a soluzioni commerciali quali Virtuozzo/OpenVZ e LXC dal 2008, su cui Docker si è basato fino al 2014.

Contenitori contro Macchine Virtuali

Tuttavia cosa differenzia un contenitore da una soluzione di virtualizzazione classica? Che motivi ci portano a preferire una o l’altra soluzione?

Innanzitutto, una soluzione di virtualizzazione classica quale VMware, HyperV o qemu/KVM prevede la creazione di una macchina virtuale che simuli una CPU con annesse periferiche, dischi e schede di rete. Questo ci permette, ad esempio, di far girare ambienti Windows all’interno di un server Linux. È facile intuire come questo tipo di virtualizzazione sia particolarmente pesante, per quanto possa essere accelerato dal supporto hardware presente nelle moderne CPU.

Il meccanismo di containerizzazione viene invece fornito direttamente dal kernel del sistema operativo che consente la segregazione e l’isolamento di gruppi di processi, oltre che il controllo fine delle risorse ad essi assegnate. Il vantaggio principale di questa soluzione è il suo peso quasi pari a zero, visto che non coinvolge una macchina virtuale. Fatto che permette di aumentare di gran lunga la “densitá abitativa” di un server.

Naturalmente entrambe le strade presentano pro e contro. Un hypervisor classico ci dá massima flessibilitá nella scelta dell’ambiente finale: possiamo scegliere il sistema operativo, eseguire kernel differenti da quello dell’host e gestire le risorse come se avessimo a che fare con una macchina fisica. Un contenitore invece può eseguire applicazioni scritte solo per il sistema operativo ospite e non ci da la possiblità di eseguire kernel differenti da quello in esecuzione sull’host ma ci fornisce un controllo molto più fine (sebbene più complicato) delle risorse.

Dobbiamo considerare, inoltre, che i contenitori non sono fortemente isolati fra loro come possono esserlo macchine virtuali o fisiche, al punto che se è proprio l’isolamento che stiamo cercando, conviene buttarsi su queste ultime, visto che sono in grado di darci garanzie di sicurezza maggiori.

L’ascesa di Docker

Nel 2013 Docker è diventato un po’ il sinonimo di contenitore, anche grazie ad alcune innovazioni che lo hanno portato rapidamente a soppiantare soluzioni più di basso livello come LXC.

Docker, infatti, si fonda sul concetto di “immagine”, ovvero un filesystem immutabile che può essere istanziato in contenitori. Le immagini possono anche essere impilate l’una sull’altra, risparmiando lo spazio occupato dagli strati in comune fra le varie immagini che abbiamo sul sistema. Ad esempio può esistere una immagine di base minimale con Ubuntu su cui può essere costruita una seconda immagine installando, ad esempio, Apache e MySQL.

La creazione di tali immagini avviene attraverso l’uso di un “Dockerfile”, ovvero una ricetta che consente la riproducibilità totale del processo di creazione dell’immagine. Nell’esempio di seguito possiamo vedere come sia facile creare una immagine per applicazioni Node.JS:

FROM ubuntu:14.04
ENV DEBIAN_FRONTEND noninteractive
RUN sed 's/archive./it.archive./g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y curl
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
RUN apt-get install -y nodejs
RUN mkdir /var/www
ADD app.js /var/www/app.js
EXPOSE 8080
CMD ["/usr/bin/node", "/var/www/app.js"]

Una volta creata l’immagine possiamo lanciarla con un semplice comando una, due, tre o mille volte con un semplice comando:

docker run -P nodejs-example

Ricordiamoci inoltre che le immagini sono immutabili: i vari contenitori la useranno come base ma le modifiche fatte al loro interno andranno perse nel momento in cui decideremo di distruggere il contenitore stesso. Come è giusto aspettarsi, Docker ci permette di dichiarare porzioni di filesystem che non devono esser volatili attraverso il sistema dei volumi, per i quali si rimanda alla documentazione ufficiale.

I vantaggi di Docker, tuttavia, non si fermano qui: una volta creata una immagine possiamo pubblicarla su un registro centralizzato, chiamato Docker Hub, per condividerla con il resto della comunità, semplificando ulteriormente il processo di lancio. Docker proverà automaticamente a scaricare l’immagine dal registro se non è presente in locale sulla macchina. È possibile anche creare ed utilizzare registri privati ed esportare le immagini come tarball da distribuire tramite le classiche chiavette USB.

Tutto ciò semplifica enormemente il processo di installazione di una applicazione. Per fare un esempio: l’installazione tipica di Mattermost (alternativa open source a Slack) richiede la configurazione separata di PostgreSQL e Nginx, oltre che di Mattermost stesso. L’operazione può richiedere almeno mezz’ora per un sistemista esperto.

Grazie a Docker ed un semplice comando siamo in grado di fare la stessa cosa in due minuti:

docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform

Successivamente, puntando il browser sulla porta 8065 della macchina sarà possibile completare la procedura di configurazione standard di Mattermost.

Docker come alternativa al gestore di pacchetti

Dal momento che le immagini sono facili da creare e possono contenere tutte le dipendenze di cui una applicazione ha bisogno, è facile pensare a Docker come una alternativa ai classici gestori di pacchetti, almeno per quanto riguarda le applicazioni web.

Docker ci offre infatti la possibilità di non doverci più preoccupare della versione di Ubuntu, Python o del runtime du-jour installato sul sistema, visto che tutti i file e tutte le librerie di cui l’applicazione ha bisogno sono incluse nell’immagine. Lo sviluppatore deve semplicemente creare una immagine basata sulla sua distribuzione preferita, includere tutte le dipendenze e distribuirla attraverso Docker Hub. L’immagine potrà essere lanciata senza problemi su CentOS, Fedora, SUSE, Arch Linux o qualsiasi altra distribuzione Linux.

Non è un caso infatti che recentemente sia nata la Open Container Initiative, che specifica un formato di pacchettizzazione delle immagini portabile fra i vari motori di containerizzazione.

Approfondimenti

In questi ultimi due anni abbiamo assistito ad una esplosione cambriana di soluzioni che girano intorno ai contenitori. Alcune basate su Docker, altre no. Di seguito elenchiamo brevemente alcuni strumenti e tecnologie interessanti, lasciando ad un futuro articolo il compito di chiarire pregi e difetti di ciò che si trova attualmente sul mercato.

Tool accessori:

Soluzioni basate su Docker: