Un’introduzione amichevole per principianti a Containers, VMs e Docker

Un’introduzione amichevole per principianti a Containers, VMs e Docker

Se sei un programmatore o semplicemente un amante della tecnologia, ci sono delle possibilità che tu abbia sentito parlare di Docker: uno strumento utile per la creazione di immagini server, il loro posizionamento ed il running di applicativi all’interno di esse, mediante l’utilizzo di “Containers“.

Grazie a tutta l’attenzione che questa nuova metodologia sta ricevendo negli ultimi mesi, grandi colossi come Google, VMware e Amazon stanno costruendo servizi per supportarla.

Indipendentemente dal fatto che abbiate o meno un caso d’uso immediato in mente per Docker, penso ancora che sia importante capire alcuni dei concetti fondamentali intorno a cosa sia un “contenitore” e come si paragona a una macchina virtuale (VM).

Mentre Internet è pieno di eccellenti guide di utilizzo per Docker, non sono riuscito a trovare molte guide concettuali per i principianti, in particolare su cosa sia un contenitore. Quindi, si spera, questo post risolva il problema 🙂

Iniziamo col capire cosa sono le macchine virtuali e i container.

Cosa sono i “containers” e le “VMs”?

Contenitori e macchine virtuali sono simili nei loro obiettivi: isolare un’applicazione e le sue dipendenze in un’unità autonoma che può essere eseguita ovunque.

Inoltre, i contenitori e le macchine virtuali eliminano la necessità di hardware fisico, consentendo un uso più efficiente delle risorse di elaborazione, sia in termini di consumo energetico che di efficacia dei costi.

La principale differenza tra contenitori e le macchine virtuali è nel loro approccio architettonico. Diamo un’occhiata più da vicino.


Macchine virtuali (Virtual Machines)

Una VM è essenzialmente un’emulazione di un vero computer che esegue programmi come un vero computer. Le macchine virtuali girano su una macchina fisica usando un “hypervisor”. Un hypervisor, a sua volta, funziona su una macchina host o su “bare metal“.

Andiamo ad analizzare la precedente definizione:

Un hypervisor è un pezzo di software, firmware o hardware su cui le VM sono eseguite. Gli hypervisor vengono eseguiti su computer fisici, denominati “host machine”. La macchina host fornisce alle macchine virtuali risorse, tra cui RAM e CPU. Queste risorse sono suddivise tra VM e possono essere distribuite come meglio si crede. Pertanto, se una VM esegue un’applicazione con una maggiore quantità di risorse, è possibile allocare più risorse a quella rispetto alle altre macchine virtuali in esecuzione sullo stesso computer host.

La VM in esecuzione sul computer host (anche in questo caso, utilizzando un hypervisor) viene spesso chiamata anche “macchina guest”. Questa macchina guest contiene sia l’applicazione che qualsiasi altra cosa necessaria per eseguire tale applicazione (ad esempio file binari e librerie di sistema). Trasporta anche un intero stack hardware virtualizzato, inclusi adattatori di rete, storage e CPU virtualizzati, il che significa che ha anche il proprio sistema operativo guest completo. Dall’interno, la macchina ospite si comporta come una propria unità con le proprie risorse dedicate.

Come accennato in precedenza, una macchina guest può essere eseguita su un hypervisor ospitato o su un hypervisor bare metal. Ci sono alcune importanti differenze tra loro.

Innanzitutto, un hypervisor di virtualizzazione ospitato, viene eseguito sul sistema operativo del computer host. Ad esempio, un computer che esegue OSX può avere una VM (ad esempio VirtualBox o VMware Workstation 8) installata su tale OS. La VM non ha accesso diretto all’hardware, quindi deve passare attraverso il sistema operativo host (nel nostro caso, l’OSX del Mac).

Il vantaggio di un hypervisor ospitato è che l’hardware sottostante è meno importante. Il sistema operativo dell’host è responsabile per i driver hardware invece dell’hypervisor stesso, ed ha quindi più “compatibilità hardware”. D’altra parte, questo strato aggiuntivo tra l’hardware e l’hypervisor crea più risorse generali, e di conseguenza abbassa le prestazioni della VM.

Un ambiente hypervisor bare metal affronta il problema delle prestazioni installando e eseguendo dall’hardware della macchina host. Poiché si interfaccia direttamente con l’hardware sottostante, non ha bisogno di un sistema operativo host per l’esecuzione. In questo caso, la prima cosa installata sul server di una macchina host come sistema operativo sarà l’hypervisor. A differenza dell’hypervisor ospitato, un hypervisor bare metal ha i propri driver di dispositivo e interagisce con ciascun componente direttamente per qualsiasi I / O, elaborazione o attività specifiche del sistema operativo. Ciò si traduce in migliori prestazioni, scalabilità e stabilità. Il compromesso qui è che la compatibilità hardware è limitata, perché l’hypervisor può contenere un numero limitato di driver per dispositivo integrati.

Dopo tutto questo discorso sugli hypervisor, ci si potrebbe chiedere perché abbiamo bisogno di questo ulteriore strato tra la VM e la macchina host.

Bene, dal momento che la VM ha un proprio sistema operativo virtuale, l’hypervisor svolge un ruolo essenziale nel fornire alle macchine virtuali una piattaforma per gestire ed eseguire questo sistema operativo guest. Consente ai computer host di condividere le proprie risorse tra le macchine virtuali in esecuzione come ospiti su di esse.

Diagramma VM

Come puoi vedere nel diagramma, le VM comprimono l’hardware virtuale, un kernel (cioè il SO) e lo spazio utente per ogni nuova VM.

Container

A differenza di una VM che fornisce la virtualizzazione dell’hardware, un container fornisce la virtualizzazione a livello di sistema operativo estraendo lo “spazio utente”. Vedrai cosa intendo mentre analizziamo il termine “contenitore”.

Per tutti gli intenti e gli scopi, i contenitori hanno l’aspetto di una VM. Ad esempio, dispongono di spazio privato per l’elaborazione, possono eseguire comandi come root, avere un’interfaccia di rete privata e un indirizzo IP, consentire percorsi personalizzati e regole iptable, possono montare i file system, ecc.

L’unica grande differenza tra contenitori e macchine virtuali è che i contenitori condividono il kernel del sistema host con altri contenitori.

Diagramma di un Container

Questo diagramma mostra che i contenitori racchiudono solo lo spazio utente e non il kernel o l’hardware virtuale come fa una VM. Ogni contenitore ottiene il proprio spazio utente isolato per consentire l’esecuzione di più contenitori su un singolo computer host. Possiamo vedere che tutta l’architettura a livello di sistema operativo viene condivisa tra i contenitori. Le uniche parti create da zero sono i contenitori e le librerie. Questo è ciò che rende i container così leggeri.


Da dove proviene Docker?

Docker è un progetto open-source basato su contenitori Linux. Usa le funzionalità del kernel di Linux come namespace e gruppi di controllo per creare contenitori su un sistema operativo.

I container sono lontani dall’essere una tecnologia giovane; Google li utilizza da anni, insieme ad altri colossi come Solaris Zones, BSD jail e LXC.

Allora perché Docker all’improvviso si sta facendo spazio tra le soluzioni server più complete al mondo?

  1. Facilità di utilizzo: Docker ha reso molto più semplice per chiunque – sviluppatori, amministratori di sistema, sistemisti e altri – sfruttare i contenitori per creare e testare rapidamente applicazioni. Consente a chiunque di impacchettare un’applicazione sul proprio laptop, che a sua volta può essere eseguita senza modifiche su qualsiasi cloud pubblico, cloud privato o persino bare metal.
  2. Velocità: I contenitori Docker sono molto leggeri e veloci. Poiché i contenitori sono solo ambienti in modalità sandbox che funzionano sul kernel, occupano meno risorse. È possibile creare ed eseguire un contenitore Docker in pochi secondi, rispetto alle macchine virtuali che potrebbero impiegare più tempo perché devono avviare ogni volta un sistema operativo virtuale completo.
  3. Docker Hub: Gli utenti di Docker beneficiano anche del sempre più ricco ecosistema di Docker Hub, che puoi considerare come un “app store per immagini Docker”. Docker Hub ha decine di migliaia di immagini pubbliche create dalla community che sono prontamente disponibili per l’uso. È incredibilmente facile cercare immagini che soddisfino le tue esigenze, pronte a essere utilizzate e abbinate a modifiche minime o nulle.
  4. Modularità e Scalabilità: Docker semplifica la suddivisione delle funzionalità dell’applicazione in singoli contenitori. Ad esempio, potresti avere il tuo database Postgres in esecuzione in un contenitore e il tuo server Redis in un altro mentre l’app Node.js si trova in un altro. Con Docker, è diventato più facile collegare questi contenitori per creare la tua applicazione, rendendo più semplice scalare o aggiornare i componenti in modo indipendente in futuro.

Ultimo ma non meno importante, chi non ama la balena Docker? 😉

Docker: concetti fondamentali

Ora che abbiamo messo in atto il quadro generale, passiamo alle parti fondamentali di Docker, pezzo per pezzo:

Docker Engine

Il Docker Engine è il livello su cui è in esecuzione Docker. È un server runtime molto leggero e contiene al suo interno, tutti gli strumenti per la gestione dei contenitori, immagini, build e altro ancora. Funziona nativamente su sistemi Linux ed è composto da:

  1. Un demone Docker eseguito nel computer host.
  2. Un client Docker che comunica con il demone per eseguire comandi.
  3. Un’API REST per interagire con il demone Docker in remoto.

Docker Client

Il client Docker è ciò che tu, come utente finale di Docker, utilizzi per la comunicazione con tutta l’infrastruttura. Pensala come l’interfaccia utente per Docker. Ad esempio, quando fai …

docker build iampeekay/someImage .

stai comunicando con il client Docker, che quindi comunica le tue istruzioni al demone Docker.

Docker Daemon

Il Docker Daemon è ciò che effettivamente esegue i comandi inviati al Docker client – come la costruzione, l’esecuzione e la distribuzione dei contenitori. Il demone viene eseguito sul computer host, ma l’utente finale non comunica mai direttamente con esso. Il client può essere eseguito anche sul computer host, ma non è necessario. Può essere eseguito su un altro computer e comunicare con il demone in esecuzione sul computer host.

Dockerfile

Un Dockerfile è dove scrivi le istruzioni per costruire un’immagine Docker. Queste istruzioni possono essere:

  • RUN apt-get y install some-package: per installare un pacchetto software
  • EXPOSE 8000: per esporre una porta
  • ENV ANT_HOME /usr/local/apache-ant per passare una variabile d’ambiente

e così via. Una volta configurato il Dockerfile, puoi utilizzare il docker build per creare un’immagine da esso. Ecco un esempio di un Dockerfile:

# Start with ubuntu 14.04
FROM ubuntu:14.04

MAINTAINER preethi kasireddy iam.preethi.k@gmail.com

# For SSH access and port redirection
ENV ROOTPASSWORD sample

# Turn off prompts during installations
ENV DEBIAN_FRONTEND noninteractive
RUN echo "debconf shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections
RUN echo "debconf shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections

# Update packages
RUN apt-get -y update

# Install system tools / libraries
RUN apt-get -y install python3-software-properties \
    software-properties-common \
    bzip2 \
    ssh \
    net-tools \
    vim \
    curl \
    expect \
    git \
    nano \
    wget \
    build-essential \
    dialog \
    make \
    build-essential \
    checkinstall \
    bridge-utils \
    virt-viewer \
    python-pip \
    python-setuptools \
    python-dev

# Install Node, npm
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
RUN apt-get install -y nodejs

# Add oracle-jdk7 to repositories
RUN add-apt-repository ppa:webupd8team/java

# Make sure the package repository is up to date
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list

# Update apt
RUN apt-get -y update

# Install oracle-jdk7
RUN apt-get -y install oracle-java7-installer

# Export JAVA_HOME variable
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle

# Run sshd
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo "root:$ROOTPASSWORD" | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

# Expose Node.js app port
EXPOSE 8000

# Create tap-to-android app directory
RUN mkdir -p /usr/src/my-app
WORKDIR /usr/src/my-app

# Install app dependencies
COPY . /usr/src/my-app
RUN npm install

# Add entrypoint
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

CMD ["npm", "start"]

Immagine Docker (Docker Image)

Le immagini sono modelli di sola lettura create da una serie di istruzioni scritte nel Dockerfile. Esse definiscono le caratteristiche dell’applicazione, delle sue dipendenze e quali processi eseguire quando vengono lanciate.

L’immagine Docker è costruita usando un Dockerfile. Ogni istruzione nel Dockerfile aggiunge un nuovo “livello” all’immagine.
I livelli rappresentano porzioni del file system delle immagini. Sono fondamentali per la struttura leggera ma potente di Docker. Docker utilizza uno Union FIle System per ottenere ciò:

Union File Systems

Docker utilizza lo Union File Systems per creare le sue immagini. Per poterlo immaginare empiricamente, si potrebbe pensare ad una struttura impilabile, il che significa che file e directory di file system separati (noti come rami) possono essere sovrapposti in modo trasparente per formare un singolo file system.

I contenuti delle directory che hanno lo stesso percorso all’interno dei rami sovrapposti sono visti come una singola directory unita, che evita la necessità di creare copie separate di ogni livello. Invece, a tutti possono essere dati dei puntatori alla stessa risorsa; quando alcuni livelli devono essere modificati, verrà creata una nuova copia modificandone una locale, lasciando l’originale invariata. È così che i file system possono “apparire” scrivibili senza consentire effettivamente le scritture. (In altre parole, un sistema “copia su scrittura”.)

I sistemi a strati offrono due vantaggi principali:

  1. Duplication-free: i livelli consentono di evitare la duplicazione di un set completo di file ogni volta che si utilizza un’immagine per creare ed eseguire un nuovo contenitore, rendendo l’istanziazione dei contenitori docker molto rapida ed economica.
  2. Layer segregation: Fare una modifica è molto più veloce: quando si modifica un’immagine, Docker propaga solo gli aggiornamenti al livello che è stato modificato.

Volumi

I volumi sono la parte “dati” di un contenitore, sono inizializzati quando viene creato un contenitore. Ti consentono di mantenere e condividere i dati fisici. Sono separati dallo Union File System predefinito e esistono come normali directory e file sul filesystem host. Quindi, anche se distruggi, aggiorni o ricostruisci il tuo contenitore, i volumi di dati rimarranno inalterati. (Come bonus aggiuntivo, i volumi di dati possono essere condivisi e riutilizzati tra più contenitori, il che è piuttosto vantaggioso.)


Containers Docker

Un contenitore Docker, come discusso sopra, avvolge il software di un’applicazione in una scatola invisibile con tutto ciò che l’applicazione deve eseguire. Ciò include il sistema operativo, il codice dell’applicazione, il runtime, gli strumenti di sistema, le librerie di sistema e così via. I contenitori Docker sono creati con immagini Docker. Poiché le immagini sono di sola lettura, Docker aggiunge un file system in lettura-scrittura sul file system di sola lettura dell’immagine per creare un contenitore.

Inoltre, creando il contenitore, Docker crea un’interfaccia di rete in modo che il contenitore possa comunicare con l’host locale, collegare ad esso un indirizzo IP disponibile, ed eseguire il processo specificato al fine di eseguire l’applicazione al momento della definizione dell’immagine.

Una volta creato un contenitore, puoi eseguirlo in qualsiasi ambiente senza dover apportare modifiche.

Approfondiamo i “Containers”

Una cosa che mi ha sempre incuriosito è il modo in cui un container viene effettivamente implementato, soprattutto perché non esiste alcun limite di infrastruttura astratta attorno ad esso. Dopo molte letture ed approfondimenti, tutto ha senso quindi ecco il mio tentativo di spiegartelo! 🙂

Il termine “contenitore” è in realtà solo un concetto astratto per descrivere come alcune caratteristiche diverse lavorano insieme per permetterne la visualizzazione. Esaminiamoli molto velocemente.

1) Namespaces

I namespace forniscono ai container la loro interfaccia del sistema Linux sottostante, limitando ciò che il container può vedere e accedere. Quando si esegue un contenitore, Docker crea dei namespaces che verranno utilizzati da esso stesso.

Esistono diversi tipi di namespaces in un kernel che Docker utilizza, ad esempio:

a. NET: Fornisce un contenitore con l’interfaccia di gestione dello stack di rete del sistema (ad esempio i propri dispositivi di rete, gli indirizzi IP, le tabelle di routing IP, / proc / net directory, numeri di porta, ecc.).
b. PID: PID sta per Process ID. Se hai mai eseguito ps aux nella riga di comando per verificare quali processi sono in esecuzione sul tuo sistema, avrai visto una colonna chiamata “PID”. Lo spazio dei nomi PID offre ai container il proprio spazio tra i processi.
c. MNT: Fornisce a un container l’interfaccia per la gestione dei “mount” sul sistema. Pertanto, per ogni tipo di volume o altro dispositivo “montato” sul container, avrai interfacce diverse.
d. UTS: UTS è l’acronimo di UNIX Timesharing System. Consente a un processo di mostrare gli identificatori di sistema (ad esempio nome host, nome dominio, ecc.). UTS consente ai contenitori di avere il proprio nome host e il nome dominio NIS, indipendente dagli altri contenitori e dal sistema host.
e. IPC: IPC è l’acronimo di InterProcess Communication. Questo namespace è responsabile dell’isolamento delle risorse IPC tra i processi in esecuzione all’interno di ciascun contenitore.
f. USER: Questo namespace viene utilizzato per isolare gli utenti all’interno di ciascun contenitore. Funziona consentendo ai contenitori di avere una vista diversa degli intervalli uid (user ID) e gid (group ID), rispetto al sistema host. Di conseguenza, l’uid e il gid di un processo possono essere diversi all’interno e all’esterno di un namespace, il che consente anche a un processo di disporre di un utente non privilegiato all’esterno di un contenitore senza sacrificare il privilegio di root all’interno di un altro contenitore.

Docker utilizza questi namespaces per la creazione di un nuovo contenitore. La funzione successiva è chiamata gruppi di controllo.

2) Gruppi di controllo

I gruppi di controllo (detti anche cgroup) sono una funzionalità del kernel Linux che isola, dà la priorità e tiene conto dell’utilizzo delle risorse (CPU, memoria, I / O del disco, rete, ecc.) Di un insieme di processi. In questo senso, un cgroup garantisce che i contenitori Docker utilizzino solo le risorse di cui hanno bisogno e, se necessario, impostano i limiti delle risorse che un container “può” utilizzare. I Cgroup assicurano inoltre che un singolo contenitore non esaurisca una di queste risorse e abbassi l’intero sistema.

3) “Isolated Union file system”:

Già descritti in alto nella sezione delle immagini Docker.

Questo è tutto ciò che c’è in un contenitore Docker (ovviamente, il difficile è nei dettagli di implementazione – come ad esempio la gestione delle interazione fra i vari componenti).

Il futuro di Docker: Docker e le VMs coesisteranno

Mentre Docker sta sicuramente guadagnando molto in termini di visibilità, non credo che diventerà una vera minaccia per le Virtual Machines. I contenitori continueranno a guadagnare terreno, ma ci sono molti casi d’uso in cui le macchine virtuali sono ancora più adatte.

Ad esempio, se è necessario eseguire più applicazioni su più server, probabilmente ha senso utilizzare le VM. D’altra parte, se è necessario eseguire molte “copie” di una singola applicazione, Docker offre alcuni vantaggi interessanti.

Inoltre, mentre i contenitori ti consentono di suddividere la tua applicazione in parti discrete più funzionali per creare una separazione di “preoccupazioni”, significa anche che c’è un numero crescente di parti da gestire, che può diventare poco pratico.

La sicurezza è anche un’area di preoccupazione per i container Docker – poiché i contenitori condividono lo stesso kernel, la barriera tra i contenitori è più sottile. Mentre una VM completa può emettere solo hypercall sull’hypervisor dell’host, un container Docker può creare syscalls sul kernel dell’host, che crea un’area di superficie più ampia per l’attacco. Quando la sicurezza è particolarmente fondamentale, è probabile che gli sviluppatori scelgano macchine virtuali, che sono isolate dall’hardware astratto, rendendo molto più difficile interferire tra loro.

Ovviamente, problemi come la sicurezza troveranno una via di risoluzione, in quanto i contenitori hanno ulteriore controllo da parte degli utenti, in fase di produzione. Per ora, il fatto che ci sia dibattito tra Containers e VMs rappresenta una marcia in più verso l’evoluzione.

Conclusioni

Spero che tu ora sia dotato delle conoscenze necessarie per saperne di più su Docker e magari usarlo in un progetto un giorno.


3 comments

Leave a Comment

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *