Articolo precedente -> Introduzione pt.2
Continuiamo la serie di articoli introduttivi a Docker, con una nuova puntata dedicata a due elementi fondamentali in un ecosistema di container: volumi e connettività. Ossia, come far parlare due container tra di loro ed elaborare i dati in una determinata cartella sull'host.
Storage: volumi e bind-mount
I file creati all'interno di un container sono conservati su un layer scrivibile dal container stesso per impostazione predefinita, con delle conseguenze significative:
- i dati non sopravvivono ad un riavvio o alla distruzione del container.
- i dati difficilmente possono essere esportati al di fuori del container se sono utilizzati da processi.
- il layer scrivibile dal container è strettamente connesso all'host sul quale gira il container stesso, ed è altrettanto difficile muovere dati tra host.
- il layer scrivibile richiede un driver dedicato che ha un impatto in termini di prestazioni.
Docker risolve questi problemi permettendo al container di eseguire operazioni I/O direttamente sull'host tramite volumi e bind-mount.
I volumi sono gestiti direttamente da Docker e non dipendono dalla struttura dell’host e questo permette ai volumi di essere più facili da migrare e backuppare, gestiti via CLI tramite le API Docker, essere condivisi in sicurezza tra container diversi, essere pre-popolati da altri container e i driver dedicati offrono cifratura, ripristino in cloud ed estensione delle funzionalità.
Altro utilizzo interessante dei volumi è la possibilità di eseguire backup (e ripristino): come visto nei precedenti articoli, una peculiarità dei container Docker è l’essere effimeri, dunque la loro struttura può essere ricreata facilmente; rimangono esclusi i dati creati e modificati. La possibilità di gestire volumi indipendenti di fatto abilita una soluzione di backup (dove viene creato, ad esempio, un archivio .tar del contenuto del volume) e ripristino (laddove viene estratto il contenuto del precedente archivio nel volume associato al container nuovamente lanciato).
In genere sono conservati sull'host generalmente nel percorso /var/lib/docker/volumes, mentre gli altri processi dell'host non possono apportare modifiche.
docker volume create nome-volume
è il comando usato per creare un volume, che può essere assegnato a più container contemporaneamente e che rimane a disposizione di Docker anche se nessun container ne richiede l'uso, mentre docker volume prune è il comando per cancellare tutti i volumi non utilizzati.
Dopo aver creato il volume, questo va associato al container con il flag --volume (-v) o --mount; Docker consigli quest’ultima sintassi.
I comandi sono del tipo:
docker run -d --name test-apache --mount source=nome-volume,target=/data apache
oppure
run -d --name test-apache --volume nome-volume:/data apache.
In entrambi i casi è stato montato il volume nome-volume nel percorso /data del container.
Il comando docker volume ls consente di elencare i volumi mappati, che possono essere ispezionati con il comando exec (che permette di eseguire comandi come se si fosse in una shell, vedi articolo precedente), ad esempio
docker container exec nome-container ls -halt /data
Le bind-mount, a differenza dei volumi, sono conservate ovunque nell'host, e sono accessibili a tutti i processi dell'host stesso; possono far riferimento (tramite percorso assoluto) a cartelle, file e configurazioni di sistema che sono quindi modificabili dal container: questa situazione deve essere correttamente valutata onde evitare problemi di sicurezza.
Per creare una bind-mount si usano i flag --volume o --mount in fase di creazione del container, con analoghe considerazioni fatte nel caso dei volumi: la seconda sintassi, da preferirsi, è più completa della prima. In entrambi i casi va specificato il percorso su host che deve comparire nel container; con il flag --volume è un’istruzione del tipo
--volume /var/www/html:/data:ro
mentre con il flag --mount è del tipo
--mount type=bind,source=/var/www/html,target=/data,readonly
Da notare come il flag --volume consista in una tripletta di dati separati da “due punti” (percorso host, percorso container, e istruzioni opzionali come modalità di sola lettura), mentre l’altro flag è più specifico e chiaro.
C'è un terzo tipo di storage, le mount tmpfs, utili soprattutto quando non si vuole che i dati rimangano su host o all'interno del container per motivi di sicurezza o prestazioni.
Le mount tmpfs, che sono supportate solo da Linux, sono create al di fuori del layer di scrittura del container, quindi sono temporanee e non sopravvivono ad un riavvio dell'host o alla distruzione del container.
Uno spazio dati di questo tipo si crea con il flag --tmpfs (che non accetta nessun parametro) o con --mount (che invece supporta indicazioni riguardo dimensioni e permessi): ad esempio
docker run -d -it --name container-tmp --mount type=tmpfs,tmpfs-size=1G,tmpfs-mode=1777,destination=/data apache
oppure
docker run -d -it --name container-tmp --tmpfs /data apache
Se invece si sceglie di utilizzare uno spazio storage nello spazio scrivibile dal container, cioè se si vogliono mantenere i dati all'interno del container stesso, occorre utilizzare un apposito driver che consenta al container di interfacciarsi direttamente con il layer sottostante; Docker fornisce driver dedicati per diversi filesystem, tra cui ext4, btrfs e zfs.
La scelta di lasciare dei dati direttamente su container non è ottimale ma in alcuni casi, in particolare certi workload specifici, si rende necessaria: bisogna comunque tener conto che in questo caso i dati non sopravvivono al riavvio o alla distruzione del container e le prestazioni in lettura e scrittura sono in genere mediocri.
Docker supporta storage a blocchi, a oggetti e a file; la scelta del tipo di storage da utilizzare viene fatta in accordo alla destinazione d’uso dei container.
Connettività
Come in ogni sistema informatico, l’argomento relativo alla connettività assume un certo rilievo, a maggior ragione quando si parla di una tecnologia come Docker che consente di creare i cosidetti “micro-servizi” e sistemi estremamente frammentati in cui ogni singola entità (container) deve essere correttamente connessa.
Con Docker è possibile creare reti container-to-container e container-to-host, gestite da driver appositi, che abilitano le rispettive modalità: bridge, host, overlay, macvlan e none; sono disponibili plugin di terze parti, come Weave.
Un container non sfrutta una scheda di rete emulata come una VM, piuttosto usa le interfacce di rete fisiche o virtuali dell’host, come eth0; i suddetti driver regolano il modo con cui host e container comunicano. Da un punto di vista interno il container vede uno stack di rete completo: indirizzo IP, sottorete di appartenenza, gateway predefinito (e VLAN se presente), tabella di instradamento, servizi DNS, etc..; l’indirizzo IP è dato direttamente dal daemon di Docker (che, pescandolo dal pool di indirizzi della sua rete a disposizione, si comporta effettivamente da server DHCP per i suoi container), il nome del container viene passato come nome host (in alcuni casi, viene risolto correttamente). Le impostazioni DNS vengono ereditate dall’host, ma possono essere impostate diversamente tramite appositi flag in fase di creazione della rete.
La modalità host è la più semplice da configurare (non vengono eseguite operazioni sul traffico e non serve configurazione specifica) ed intuitiva: viene meno la separazione tra container ed host dal momento che questo utilizza la scheda di rete (NIC) fisica dell’host, quindi assume lo stesso indirizzo IP dell’host e, molto importante, fa parte dello stesso spazio (e quindi lo consuma) delle porte TCP. In altre parole, non è possibile avere due container sullo stesso host in modalità host a cui è assegnata la stessa porta.
Un comando del tipo
docker run -d --name apache --network=host apache
consente di creare un container con connettività in modalità host.
La modalità bridge consente di superare le difficoltà relative all’assegnazione delle porte creando un namespace di rete privato e interno all’host (dunque senza le implicazioni riguardo la sicurezza della modalità host), quindi il container ottiene il suo indirizzo IP e tutte le porte a disposizione. In seguito, iptables tramite NAT (Network Address Translation) esegue la mappatura tra host e container; a causa di ciò c’è una diminuzione delle prestazioni.
In questa maniera è possibile creare, ad esempio, due container Apache in ascolto su porta 80 (o 443) e con indirizzo IP proprio che sono mappati su due porte differenti dell’host. L’accesso dall’esterno sarà semplicemente ip-server:porta-1 per il primo container e ip-server:porta-2 per il secondo.
Un comando del tipo docker run -d --name apache -p 8080:80 apache consente di creare un container con connettività in modalità bridge mappando la porta 80 del container sulla porta 8080 dell’host (il mapping delle porta funziona tramite host:container). Non è necessario specificare il parametro --net=bridge dato che è quello predefinito e viene usato il bridge predefinito dell’host..
Il comando docker network create --driver bridge nome-rete crea una rete di tipo bridge user-defined, che tramite il servizio “automatic service discovery” risolve correttamente i nomi dei container in indirizzi IP.
Se si cerca di far comunicare container su host differenti le modalità host e bridge, che si occupano di gestire i container interni allo stesso host (il cui kernel Linux, in sostanza, gestisce la connettività) non bastano.
La modalità overlay supera questo problema permettendo di fatto di creare semplici ambienti con diversi host disponibili o strutture complesse orchestrate da Docker Swarm o Kubernetes.
Viene richiesto un piccolo lavoro di configurazione su ciascun host interessato, in particolare vanno aperte determinate porte TCP e UDP per le comunicazioni di gestione intra-cluster, tra nodi e per la rete overlay, inoltre va aggiunto ad uno swarm o multi-swarm (tratteremo questo argomento in articolo a parte: per ora basti sapere che si tratta di una modalità di Docker che consente di gestire cluster di host) esistente o ne va creato uno.
È questo il tipo di rete usato nei cluster e, di conseguenza della popolarità dell’uso di soluzioni di orchestrazioni, il più usato in genere. Anche in questo caso è possibile mappare porte container:host.
Il comando docker network create --opt encrypted --driver overlay --attachable nome-rete crea una rete di tipo overlay utilizzabile dai membri dello swarm e con traffico -dati e di gestione- cifrato.
La modalità macvlan mette in comunicazione il container direttamente con l’interfaccia di rete e permette di assegnare un indirizzo MAC alle interfacce di rete virtuali facendole apparire come interfacce fisiche connesse direttamente alla rete fisica: i container possono apparire, con indirizzo IP proprio, sulla stessa sottorete dell’host e possono comunicare con risorse esterne all’host senza port mapping e NAT. Inoltre è possibile sfruttare le VLAN create sul NIC fisico.
È richiesto un NIC sull’host con supporto alla modalità promiscua (promiscuous mode: supporto ad indirizzi MAC multipli) e l’assegnazione di sottorete e gateway.
Questa modalità risulta utile in quei casi in cui ci si aspetta di usare componenti che richiedono indirizzo MAC (ad esempio sistemi di monitoring); il comando docker container inspect nome-container restituisce la configurazione del container, e il contenuto della parte Network contiene dati sulll’indirizzo MAC assegnato. Similmente, l’informazione è ottenibile facendo eseguire al container dei comandi interni come docker exec nome-container ip addr show oppure docker exec nome-container ip route.
Il comando seguente crea una rete di tipo macvlan sul NIC eth0 con VLAN 90.
docker network create --driver macvlan --subnet=192.168.0.0/24 --gateway=192.168.0.1 -o parent=eth0.90 nome-rete
Infine la modalità none: come il nome lascia intendere, con questo driver il container riceve un network stack ma non un’interfaccia di rete esterna; riceve un’interfaccia di loopback.
La modalità none trova un ambito d’uso in container usati per test, container che riceveranno in seguito una connessione e, naturalmente, container che non hanno bisogno di connettività esterna.
A questo punto dovremmo avere una buona conoscenza di base dei container Docker: nelle prossime puntate vedremo come gestire host e container Docker con gli strumenti adatti.