blexin

Consulenza
Servizi e
Formazione it


Blog

Evolvi la tua azienda

Vediamo insieme perché, come sviluppatori, dovremmo utilizzare i container.

Chi ha detto che Docker serve solo in produzione?

Nell'ultimo anno mi sono interessato a Docker e a tutto l'ecosistema che ci gira intorno, fino ad arrivare a Kubernetes per l'orchestrazione dei container. Devo dire che contro ogni iniziale aspettativa mi sono molto appassionato alla cosa ed ho cominciato, come è mio solito, a condividere la mia esperienza agli eventi in cui tengo delle sessioni.

Faccio una premessa sul "contro ogni iniziale aspettativa": ero partito pensando che Docker & company fossero strumenti che supportano solo la fase di rilascio delle applicazioni. Quindi per evitare di fare lo stesso errore a chi come me pensa che preparare ambienti di produzione sia noioso, volevo raccontare la felice scoperta che i container sono una fantastica risorsa anche per la fase di sviluppo dell'applicazione, oltre a semplificare sensibilmente tutte le fasi successive.

Partiamo dal perché, come sviluppatore, dovrei utilizzare i container:

  1. Velocizza l'ingresso di un nuovo elemento nel team
  2. Elimina i conflitti nelle applicazioni
  3. Permette rilasci più semplici e veloci
  4. Si.. ok... rendono sostenibile la gestione dei "microservizi"

Per capire da dove viene fuori questa lista diciamoci velocemente che cosa è Docker. Stiamo parlando di una piattaforma open source per la gestione del ciclo di vita di un affare chiamato "container", il cui scopo è semplificare la creazione, il rilascio e l'esecuzione di applicazioni.

Di base partiamo da un template in sola lettura chiamato "immagine" che è sostanzialmente un file system a livelli usato per condividere i file comuni. Se siete degli sviluppatori pensate alla definizione di una classe. Una classe definisce un "template" da cui create degli oggetti. Stessa cosa qui: parto da una immagine Docker e "istanziandola" creo un container. Per continuare con il parallelismo un container è un oggetto istanza di una immagine con il suo stato interno e il suo ciclo di vita.

Docker image e container

Giusto per essere sicuri che ci stiamo capendo, avrete sicuramente dimestichezza con il concetto di macchine virtuale, dove in buona sostanza potete far girare su un sistema operativo a vostro piacere installato su una macchina fisica, un altro (o lo stesso) sistema operativo su una astrazione virtualizzata dello stesso hardware. E probabilmente sapete anche il perché le virtual machine sono utili: ambienti isolati che possono girare sullo stesso hardware, facilmente backuppabili e ripristinabili, su cui potete installare qualsiasi cosa volete.

Purtroppo il loro vantaggio è anche il loro principale limite: dovete installarci dentro il sistema operativo con un consumo di spazio e memoria, oltre che di tempi di avvio molto alti. In ambienti di produzione sono una soluzione molto conveniente per risparmiare "ferro" e avere controllo sulle singole applicazioni. In ambiente di sviluppo lavorare con le virtual machine richiede tanto spazio e tanta memoria e anche solamente copiare un VM può richiedere tempo. Un container risolve esattamente questo problema: avere un ambiente isolato e veloce, che consumi meno spazio e memoria perché condivide il sistema operativo che li ospita.

vm e container

I container nascono in ambiente Linux e fino a poco tempo fa avevate bisogno di una virtual machine Linux per poterli usare, ma essendo diventato il trend del momento (per una volta non per moda ma perché risolve un problema) anche Microsoft si è attivata per fornire su Windows gli strumenti nativi per far girare i container.

vm e container

Velocizza l'ingresso di un nuovo elemento nel team

Supponente di avere una applicazione che per funzionare ha bisogno di uno o più database, una cache, un backend e un frontend. Stiamo quindi parlando del minimo sindacale. Arriva un nuovo elemento nel team e al giorno zero, al di là delle sue competenze, avrà tre possibilità:

  1. Installare tutto sul proprio PC e configurarlo
  2. Copiarsi le VM e configurarle
  3. Scaricare i sorgenti, lanciare un comando del tipo "docker-compose up" e aspettare un po' la prima volta (deve tirare giù le immagini) per vedere la propria applicazione partire.

Penso di aver reso l'idea sottolineando che il terzo punto è lo scenario che vi implementa Docker.

Elimina i conflitti nelle applicazioni

Se il mio codice e le librerie che uso sono in un ambiente isolato che condivide il sistema operativo, ho risolto il problema delle dipendenze alle stesse librerie di diverse applicazioni o delle diverse parti di una stessa applicazione. Se non avete pensato WOW, non avete mai lavorato con JavaScript e NPM ma anche in ambienti come .NET la situazione non è delle più rosee.

Permette rilasci più semplici e veloci

Ricreare in locale l'ambiente di produzione che ha generato un problema con i container è molto più semplice, semplificando enormemente tutti i rilasci che faremo delle applicazioni.

Si.. ok... rendono sostenibile la gestione dei "microservizi"

Nella speranza che quello che intendete per microservizio sia la giusta definizione e che avete scelto un architettura a microservizi perché vi risolve un problema invece di crearne, allora l'unico modo sostenibile di gestirli in tutti gli ambienti e proprio con i container.

Facciamo un esempio

Giusto per capire di cosa stiamo parlando, creiamo insieme un ambiente di SVILUPPO con Docker, concentrandoci per questa volta sul frontend.

Create una applicazione Angular con la CLI (ng new frontend) in una cartella a vostro piacere. Nella root del progetto (la cartella frontend per intenderci) create un nuovo file il cui nome sarà Dockerfile. Un Dockerfile vi permette di scriptare le operazioni necessarie alla creazione di una immagine Docker. Si parte sempre da una immagine di base che contiene gli strumenti che ci servono per lavorare, nel nostro caso ci serve Node.JS, quindi la prima riga dello script sarà la seguente:

FROM node:10-alpine

L'istruzione FROM, come dice la parola stessa, indica l'immagine da cui vogliamo partire, che sarà scaricata da un registro pubblico chiamato Docker Hub, se la stessa non è già presente localmente. A partire da questa immagine creiamoci una cartella che chiameremo "app". Per eseguire un comando dentro un container durante la creazione di una immagine viene utilizzata l'istruzione RUN:

RUN mkdir app

Dato che tutte le operazioni che faremo saranno fatte in questa cartella impostiamo la cartella "app" come cartella corrente, utilizzando l'istruzione WORKDIR:

WORKDIR /app

A questo punto copiamo il nostro codice nella cartella utilizzando l'istruzione COPY, dalla cartella locale corrente alla cartella locale del container:

COPY . .

Se lasciamo le cose così tutta la folder di Angular sarà copiata nel container, ma ci sono alcune cartelle che non ci interessano, in particolare la cartella node_modules, perché vogliamo che le librerie che scarichiamo siano compatibili con il sistema operativo su cui girerà il container e non il nostro locale. Se avete dimestichezza con il mondo Node/JavaScript/NPM, sapete bene che questo non è un dettaglio. Andiamoci quindi a creare un file chiamato .dockerignore che ci permette di elencare le cartelle che non vogliamo che siano copiate nella nostra immagine. Dentro questo file, almeno inizialmente, ci andiamo a mettere solo node_module:

/node_modules

Tornando al nostro Dockerfile dobbiamo eseguire il download delle librerie che servono al progetto:

RUN npm install

Siamo quasi pronti. Arrivati a questo punto la nostra immagine conterrà tutti i file che ci servono. Andiamo, come ultima istruzione, a indicare il comando che vogliamo venga eseguito quando la nostra immagine viene utilizzata per creare un container, utilizzando l'istruzione CMD:

CMD $(npm bin)/ng serve --host 0.0.0.0

L'istruzione $(npm bin) serve a farsi restituire il path della cartella che contiene le nostre dipendenze e, come probabilmente saprete, la CLI di Angular è una dipendenza di sviluppo definita nel package.json. L'opzione --host serve a evitare che l'applicazione risponda su localhost, perché sarebbe il localhost del container e non il nostro. Indicando 0.0.0.0 possiamo re-indirizzare (sulla giusta porta) le richieste che vengono dalla nostra macchina host al server http di sviluppo che esegue la nostra applicazione con la CLI nel container.

Ricapitolando ecco il nostro Dockerfile di sviluppo:

FROM node:10-alpine
RUN mkdir app
WORKDIR /app
COPY . .
RUN npm install
CMD $(npm bin)/ng serve --host 0.0.0.0

Creiamo la nostra immagine da questo script con il seguente comando:

docker build -t frontend:dev-v1 .

Il comando docker build serve a costruire una immagine docker, l'opzione -t ci permette di taggare l'immagine con una nome a nostra scelta nella forma nome:versione (nel nostro caso stiamo dicendo che è una immagine di frontend di sviluppo con numero di versione v1, ma siete liberi di adottare la nomenclatura che volete). Come vedete non abbiamo indicato il nome del Dockerfile perché abbiamo usato il nome standard, se decidete di usare un nome custom, vi baste indicarlo con l'opzione -f. Infine c'è un punto, che indica il contesto corrente a cui si riferisce l'esecuzione dei comandi presenti nello script. Con il punto state dicendo che il contesto corrente è la cartella da cui state eseguendo il comando docker build, potete indicare ovviamente una cartella differente se ne avete bisogno.

Il risultato dell'esecuzione sarà il seguente:

docker build

Come vedete, per ogni istruzione viene creato un container temporaneo per eseguire il comando fino ad arrivare all'immagine finale. Con l'immagine creata potete avviare il vostro container con il seguente comando:

docker run frontend:dev-v1

Lanciato così però il container non vi da l'interattività che vi serve durante lo sviluppo, specie se vogliamo che quando modifichiamo il codice localmente la modifica si propaghi nel container. Inoltre l'applicazione Angular non è nemmeno raggiungibile perché è eseguita su una porta (la 4200) che non è esposta dal container.

container

L'esposizione della porta è molto semplice, basta indicare l'opzione -p 4200:4200 (la 4200 locale verso la 4200 del container, ovviamente quella locale può essere qualsiasi). Condividere il codice sul vostro file system con quello del container richiede la creazione di un oggetto che in Docker è chiamato Volume, e lo potete fare sullo stesso comando utilizzando l'opzione -v:

docker run -it -p 4200:4200 -v $(pwd)/src:/app/src frontend:dev-v1

Il risultato sarà il seguente:

container con porta e volume

L'opzione -it vi permette di avere una modalità interattiva con terminale TTY, molto comodo per lanciare comandi nel container come il CTRL+C.

Grazie a questa configurazione avete la stessa esperienza di sviluppo che avreste localmente ma con tutti i vantaggi dell'utilizzo dei container! Nei prossimi post vedremo come aggiungere il backend e creare uno script docker-compose (un po' di pazienza, ne parleremo la prossima volta) che fa tutto il lavoro sporco per noi!

Happy coding!

Servizi

Evolvi la tua azienda