Introduzione a Docker: semplificare DevOps

Pubblicato: 2022-03-11

Se ti piacciono le balene, o sei semplicemente interessato alla consegna continua rapida e indolore del tuo software alla produzione, ti invito a leggere questo tutorial introduttivo di Docker. Tutto sembra indicare che i container software siano il futuro dell'IT, quindi facciamo un rapido tuffo con le balene container Moby Dock e Molly.

Docker, rappresentato da un logo con una balena dall'aspetto amichevole

Docker, rappresentato da un logo con una balena dall'aspetto amichevole, è un progetto open source che facilita la distribuzione di applicazioni all'interno di contenitori software. La sua funzionalità di base è abilitata dalle funzionalità di isolamento delle risorse del kernel Linux, ma fornisce un'API di facile utilizzo in aggiunta. La prima versione è stata rilasciata nel 2013 e da allora è diventata estremamente popolare ed è ampiamente utilizzata da molti grandi giocatori come eBay, Spotify, Baidu e altri. Nell'ultimo round di finanziamento, Docker ha ottenuto ben 95 milioni di dollari ed è sulla buona strada per diventare un punto fermo dei servizi DevOps.

Analogia del trasporto di merci

La filosofia alla base di Docker potrebbe essere illustrata con una semplice analogia. Nel settore dei trasporti internazionali, le merci devono essere trasportate con mezzi diversi come carrelli elevatori, camion, treni, gru e navi. Queste merci sono disponibili in diverse forme e dimensioni e hanno diverse esigenze di stoccaggio: sacchi di zucchero, lattine di latte, piante ecc. Storicamente, era un processo doloroso che dipendeva dall'intervento manuale in ogni punto di transito per il carico e lo scarico.

Un carro trainato da cavalli, un camioncino e un camion da trasporto, tutti trasportano merci

Tutto è cambiato con l'adozione dei container intermodali. Poiché sono disponibili in dimensioni standard e sono fabbricati pensando al trasporto, tutti i macchinari pertinenti possono essere progettati per gestirli con il minimo intervento umano. L'ulteriore vantaggio dei contenitori sigillati è che possono preservare l'ambiente interno come la temperatura e l'umidità per le merci sensibili. Di conseguenza, l'industria dei trasporti può smettere di preoccuparsi delle merci stesse e concentrarsi sul portarle da A a B.

Trasporto con container via terra e via mare

Ed è qui che entra in gioco Docker e apporta vantaggi simili all'industria del software.

In che cosa differisce dalle macchine virtuali?

A prima vista, le macchine virtuali ei container Docker possono sembrare simili. Tuttavia, le loro differenze principali risulteranno evidenti quando si osserva il diagramma seguente:

Grafico di confronto di macchine virtuali (VM) e container

Le applicazioni in esecuzione su macchine virtuali, a parte l'hypervisor, richiedono un'istanza completa del sistema operativo e di tutte le librerie di supporto. I container, invece, condividono il sistema operativo con l'host. Hypervisor è paragonabile al container engine (rappresentato come Docker nell'immagine) nel senso che gestisce il ciclo di vita dei container. La differenza importante è che i processi in esecuzione all'interno dei contenitori sono proprio come i processi nativi sull'host e non introducono alcun sovraccarico associato all'esecuzione dell'hypervisor. Inoltre, le applicazioni possono riutilizzare le librerie e condividere i dati tra contenitori.

Poiché entrambe le tecnologie hanno diversi punti di forza, è comune trovare sistemi che combinano macchine virtuali e container. Un esempio perfetto è uno strumento chiamato Boot2Docker descritto nella sezione di installazione di Docker.

Architettura Docker

Architettura Docker

Nella parte superiore del diagramma dell'architettura ci sono i registri. Per impostazione predefinita, il registro principale è Docker Hub che ospita immagini pubbliche e ufficiali. Le organizzazioni possono anche ospitare i propri registri privati, se lo desiderano.

Sul lato destro abbiamo immagini e contenitori. Le immagini possono essere scaricate dai registri in modo esplicito ( docker pull imageName ) o implicitamente all'avvio di un contenitore. Una volta scaricata, l'immagine viene memorizzata nella cache locale.

I contenitori sono le istanze delle immagini: sono la cosa vivente. Potrebbero esserci più contenitori in esecuzione in base alla stessa immagine.

Al centro c'è il demone Docker responsabile della creazione, dell'esecuzione e del monitoraggio dei container. Si occupa anche della costruzione e della memorizzazione delle immagini. Infine, sul lato sinistro c'è un client Docker. Parla con il demone tramite HTTP. I socket Unix vengono utilizzati sulla stessa macchina, ma la gestione remota è possibile tramite API basata su HTTP.

Installazione Docker

Per le ultime istruzioni dovresti sempre fare riferimento alla documentazione ufficiale.

Docker funziona in modo nativo su Linux, quindi a seconda della distribuzione di destinazione potrebbe essere facile come sudo apt-get install docker.io . Fare riferimento alla documentazione per i dettagli. Normalmente in Linux, anteponi i comandi Docker con sudo , ma lo salteremo in questo articolo per chiarezza.

Poiché il demone Docker utilizza le funzionalità del kernel specifiche di Linux, non è possibile eseguire Docker in modo nativo in Mac OS o Windows. Invece, dovresti installare un'applicazione chiamata Boot2Docker. L'applicazione è composta da una macchina virtuale VirtualBox, da Docker stesso e dalle utilità di gestione Boot2Docker. Puoi seguire le istruzioni di installazione ufficiali per MacOS e Windows per installare Docker su queste piattaforme.

Utilizzando Docker

Iniziamo questa sezione con un rapido esempio:

 docker run phusion/baseimage echo "Hello Moby Dock. Hello Molly."

Dovremmo vedere questo output:

 Hello Moby Dock. Hello Molly.

Tuttavia, dietro le quinte è successo molto di più di quanto tu possa pensare:

  • L'immagine 'phusion/baseimage' è stata scaricata da Docker Hub (se non era già nella cache locale)
  • È stato avviato un contenitore basato su questa immagine
  • Il comando echo è stato eseguito all'interno del contenitore
  • Il contenitore è stato arrestato all'uscita del comando

Alla prima esecuzione, potresti notare un ritardo prima che il testo venga stampato sullo schermo. Se l'immagine fosse stata memorizzata nella cache locale, tutto avrebbe richiesto una frazione di secondo. I dettagli sull'ultimo contenitore possono essere recuperati eseguendo docker ps -l :

 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES af14bec37930 phusion/baseimage:latest "echo 'Hello Moby Do 2 minutes ago Exited (0) 3 seconds ago stoic_bardeen

Fare la prossima immersione

Come puoi vedere, eseguire un semplice comando all'interno di Docker è facile come eseguirlo direttamente su un terminale standard. Per illustrare un caso d'uso più pratico, nel resto di questo articolo vedremo come utilizzare Docker per distribuire una semplice applicazione server web. Per semplificare le cose, scriveremo un programma Java che gestisca le richieste HTTP GET a '/ping' e risponda con la stringa 'pong\n'.

 import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class PingPong { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/ping", new MyHandler()); server.setExecutor(null); server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { String response = "pong\n"; t.sendResponseHeaders(200, response.length()); OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); } } }

File Docker

Prima di entrare e creare la tua immagine Docker, è buona norma controllare prima se ce n'è una esistente nell'hub Docker o in eventuali registri privati ​​a cui hai accesso. Ad esempio, invece di installare noi stessi Java, utilizzeremo un'immagine ufficiale: java:8 .

Per costruire un'immagine, dobbiamo prima decidere su un'immagine di base che useremo. È indicato dall'istruzione FROM . Qui, è un'immagine ufficiale per Java 8 dal Docker Hub. Lo copieremo nel nostro file Java emettendo un'istruzione COPY . Successivamente, lo compileremo con RUN . L'istruzione EXPOSE indica che l'immagine fornirà un servizio su una porta particolare. ENTRYPOINT è un'istruzione che vogliamo eseguire all'avvio di un container basato su questa immagine e CMD indica i parametri predefiniti che gli passeremo.

 FROM java:8 COPY PingPong.java / RUN javac PingPong.java EXPOSE 8080 ENTRYPOINT ["java"] CMD ["PingPong"]

Dopo aver salvato queste istruzioni in un file chiamato "Dockerfile", possiamo costruire l'immagine Docker corrispondente eseguendo:

 docker build -t toptal/pingpong .

La documentazione ufficiale per Docker ha una sezione dedicata alle migliori pratiche relative alla scrittura di Dockerfile.

Contenitori in esecuzione

Quando l'immagine è stata costruita, possiamo darle vita come un contenitore. Esistono diversi modi per eseguire i container, ma iniziamo con uno semplice:

 docker run -d -p 8080:8080 toptal/pingpong

dove -p [port-on-the-host]:[port-in-the-container] indica rispettivamente la mappatura delle porte sull'host e sul container. Inoltre, stiamo dicendo a Docker di eseguire il contenitore come processo demone in background specificando -d . È possibile verificare se l'applicazione del server Web è in esecuzione tentando di accedere a 'http://localhost:8080/ping'. Si noti che sulle piattaforme in cui viene utilizzato Boot2docker, sarà necessario sostituire "localhost" con l'indirizzo IP della macchina virtuale in cui è in esecuzione Docker.

Su Linux:

 curl http://localhost:8080/ping

Su piattaforme che richiedono Boot2Docker:

 curl $(boot2docker ip):8080/ping

Se tutto va bene, dovresti vedere la risposta:

 pong

Evviva, il nostro primo container Docker personalizzato è vivo e nuota! Potremmo anche avviare il contenitore in modalità interattiva -i -t . Nel nostro caso, sovrascriveremo il comando entrypoint in modo che ci venga presentato un terminale bash. Ora possiamo eseguire tutti i comandi che vogliamo, ma uscire dal contenitore lo fermerà:

 docker run -i -t --entrypoint="bash" toptal/pingpong

Sono disponibili molte altre opzioni da utilizzare per avviare i container. Copriamone altri. Ad esempio, se desideriamo mantenere i dati al di fuori del contenitore, potremmo condividere il filesystem host con il contenitore usando -v . Per impostazione predefinita, la modalità di accesso è lettura-scrittura, ma può essere modificata in modalità di sola lettura aggiungendo :ro al percorso del volume all'interno del contenitore. I volumi sono particolarmente importanti quando è necessario utilizzare informazioni di sicurezza come credenziali e chiavi private all'interno dei contenitori, che non devono essere archiviate nell'immagine. Inoltre, potrebbe anche impedire la duplicazione dei dati, ad esempio mappando il tuo repository Maven locale sul contenitore per evitare di scaricare Internet due volte.

Docker ha anche la capacità di collegare tra loro i contenitori. I container collegati possono comunicare tra loro anche se nessuna delle porte è esposta. Può essere ottenuto con –link nome-altro-contenitore . Di seguito è riportato un esempio che combina i parametri sopra menzionati:

 docker run -p 9999:8080 --link otherContainerA --link otherContainerB -v /Users/$USER/.m2/repository:/home/user/.m2/repository toptal/pingpong

Altre operazioni su contenitori e immagini

Non sorprende che l'elenco delle operazioni che si potrebbero applicare ai contenitori e alle immagini sia piuttosto lungo. Per brevità, diamo un'occhiata ad alcuni di essi:

  • stop - Arresta un contenitore in esecuzione.
  • start - Avvia un container fermo.
  • commit: crea una nuova immagine dalle modifiche di un contenitore.
  • rm - Rimuove uno o più contenitori.
  • rmi - Rimuove una o più immagini.
  • ps - Elenca i contenitori.
  • immagini - Elenca le immagini.
  • exec: esegue un comando in un contenitore in esecuzione.

L'ultimo comando potrebbe essere particolarmente utile per scopi di debug, in quanto consente di connettersi a un terminale di un container in esecuzione:

 docker exec -i -t <container-id> bash

Docker Compose per il mondo dei microservizi

Se hai più di un paio di contenitori interconnessi, ha senso usare uno strumento come docker-compose. In un file di configurazione, descrivi come avviare i contenitori e come dovrebbero essere collegati tra loro. Indipendentemente dalla quantità di contenitori coinvolti e dalle loro dipendenze, potresti averli tutti attivi e funzionanti con un solo comando: docker-compose up .

Docker in natura

Diamo un'occhiata alle tre fasi del ciclo di vita del progetto e vediamo come la nostra amichevole balena potrebbe essere di aiuto.

Sviluppo

Docker ti aiuta a mantenere pulito il tuo ambiente di sviluppo locale. Invece di avere più versioni di diversi servizi installati come Java, Kafka, Spark, Cassandra, ecc., puoi semplicemente avviare e interrompere un contenitore richiesto quando necessario. Puoi fare un ulteriore passo avanti ed eseguire più stack di software fianco a fianco evitando il confondimento delle versioni delle dipendenze.

Con Docker puoi risparmiare tempo, fatica e denaro. Se il tuo progetto è molto complesso da configurare, "dockerizzalo". Passa attraverso il dolore di creare un'immagine Docker una volta e da questo punto tutti possono semplicemente avviare un contenitore in un attimo.

Puoi anche avere un "ambiente di integrazione" in esecuzione localmente (o su CI) e sostituire gli stub con servizi reali in esecuzione nei contenitori Docker.

Test / Integrazione Continua

Con Dockerfile, è facile ottenere build riproducibili. Jenkins o altre soluzioni CI possono essere configurate per creare un'immagine Docker per ogni build. È possibile archiviare alcune o tutte le immagini in un registro Docker privato per riferimenti futuri.

Con Docker, verifichi solo ciò che deve essere testato e togli l'ambiente dall'equazione. L'esecuzione di test su un contenitore in esecuzione può aiutare a mantenere le cose molto più prevedibili.

Un'altra caratteristica interessante dell'avere contenitori software è che è facile creare macchine slave con la stessa configurazione di sviluppo. Può essere particolarmente utile per il test di carico di distribuzioni in cluster.

Produzione

Docker può essere un'interfaccia comune tra sviluppatori e personale operativo, eliminando una fonte di attrito. Incoraggia inoltre l'utilizzo della stessa immagine/binari in ogni fase della pipeline. Inoltre, la possibilità di distribuire container completamente testati senza differenze di ambiente aiuta a garantire che non vengano introdotti errori nel processo di compilazione.

Puoi migrare senza problemi le applicazioni in produzione. Qualcosa che una volta era un processo noioso e traballante ora può essere semplice come:

 docker stop container-id; docker run new-image

E se qualcosa va storto durante la distribuzione di una nuova versione, puoi sempre eseguire rapidamente il rollback o passare a un altro container:

 docker stop container-id; docker start other-container-id

... garantito per non lasciare alcun pasticcio alle spalle o lasciare le cose in uno stato incoerente.

Sommario

Un buon riassunto di ciò che fa Docker è incluso nel suo stesso motto: Build, Ship, Run.

  • Build - Docker ti consente di comporre la tua applicazione da microservizi, senza preoccuparti delle incongruenze tra gli ambienti di sviluppo e produzione e senza bloccarti in alcuna piattaforma o linguaggio.
  • Spedisci - Docker ti consente di progettare l'intero ciclo di sviluppo, test e distribuzione delle applicazioni e di gestirlo con un'interfaccia utente coerente.
  • Esegui: Docker ti offre la possibilità di distribuire servizi scalabili in modo sicuro e affidabile su un'ampia varietà di piattaforme.

Divertiti a nuotare con le balene!

Parte di questo lavoro è ispirato da un ottimo libro Using Docker di Adrian Mouat.