Lo scopo di questa pagina è agire come primo repository di note/esperienze per quanto riguarda concetti basati su i container. Un po’ i miei primi passi come recita il titolo per capire questa interessante tecnologia, come provarla ed usarla in maniera semplice.
CONTAINER
Può essere considerato un processo “speciale” che rappresenta un applicazione che è avviata su un sistema operativo.
Speciale poichè può essere limitato e isolato nell’utilizzo delle risorse dell’host.
Il confronto con le Virtual Machine è quasi lo standard, ma ovviamente anche i container hanno bisogno di un sistema operativo, la loro natura però è quella di essere un “processo” è quindi apre scenari di portabilità più ampi con molti meno pre-requisiti e problemi rispetto alle normali applicazioni installate in maniera “standard”.
DA CHE COSA E’ COSTITUITO UN CONTAINER?
Un container è un immagine che si riferisce all’applicazione. Contiene inoltre a parte l’applicazione anche tutte le librerie e i binari necessari per poter avviare la suddetta app. Quando un container viene avviato vengono utilizzare due funzionalità del sistema operativo per potergli allocare risorse ed isolarlo dallo stesso sistema operativo dove quest’ultimo è avviato. Queste funzionalità sono chiamate rispettivamente Control Groups (cgroups) e namespaces.
Il container viene quindi avviato all’interno di un namespace dove gli vengono fornite risorse CPU e Memoria, la sua copia dell’host network stack e il suo file system.
Link a che cosa sono i containers direttamente dal Google Cloud:
https://cloud.google.com/learn/what-are-containers
Qui sotto un immagine presa direttamente da Docker.com che mostra le principali differenze tra applicazioni containerizzate e standard.
PERCHE’ USARE I CONTAINER?
La tecnologia dei container in realtà non è nata dal nulla, era già presente da molti anni ma la prima adozione “facile” è stata introdotta da Docker, che ha semplificato il lavoro degli sviluppatori permettendo di pacchettizare le applicazioni in container attraverso appunto il prodotto Docker.
Se si pensa al normale ciclo di installazione di un applicazione nella maniera tradizionale su un infrastruttura vSphere tipicamente gli step necessari ad avere tutto pronto sono i seguenti:
– Creazione della VM
– Installazione Guest OS
– Customizzazione / Configurazione OS tramite VMware Tools o Automazione
– Installazione App
Ovviamente vSphere fornisce delle funzionalità out of the box che permettono di poter gestire le VM in maniera resiliente e distribuita, HA e DRS per fare due esempi. Ma se ci concentriamo sulla gestione dell’applicazione in questo formato chiamato anche “Monolitico” possiamo già immaginare quali siano i task e le difficoltà di dover aggiornare l’applicazione: VM snapshot, interoperabilità tra diversi componenti installati nell’OS, OS diversi, librerie binari ed eventuali aggionamenti degli stessi con conseguente bisogno di testare eventuali differenze tra ambienti. (Solitamente se poi un app non funziona bene si INCOLPA sempre l’infrastuttura vSphere che la ospita)
Con i container invece è possibile dividere in diverse parti, container appunto l’applicazione, questo processo è anche detto “refactoring”, così da poter avere parti separate su cui poter lavorare indipendentemente. Questo permette al team di sviluppo di concentrarsi solo su quella parte evitando di dover interagire con tutti i componenti che prima facevano parte di un blocco unico.
Questo inoltre permette di poter sviluppare e testare in maniera consistente, mantenendo lo standard e la portabilità ed evitando così di avere differenze sostanziali tra i diversi ambienti di Test, QA e Produzione.
Ovviamente va sottolineato che il processo di “refactoring” non è cosa semplice, alcune applicazioni possono richiedere mesi se non anni di lavoro per poter modernizzare l’applicazione sottoforma di container. Quello che sicuramente sarà possibile vedere come workload all’interno di un datacenter moderno sarà sicuramente un mix di entrambi i modelli, per questo motivo è bene sapere come gestire le modern app all’interno di un ambiente vSphere o perlomeno sapere come funzionano!
INCOMINCIO DA DOCKER!
Nel titolo ovviamente c’è un po’ la domanda che ci facciamo un po’ tutti quando dobbiamo approcciare una nuova tecnologia di cui conosciamo poco e quindi la prima cosa da fare è sapere quali sono i primi passi da fare. Docker ovviamente è il punto di partenza basilare ma ci sono molte possibilità disponibili per poter cominciare soprattutto anche usando proprio il pc portatile dal quale magari stai leggendo questo articolo!
Per iniziare è possibile installare i diversi container runtimes disponibili, c’è ne sono molteplici tra cui scegliere e possono essere installati sul tuo pc in diversi modi.
Docker fornisce Docker Desktop gratuitamente a questo indirizzo: https://www.docker.com/products/docker-desktop/
Un’alternativa possibile è anche Rancher Desktop – https://rancherdesktop.io/ che è un applicazione gratuita che permette di poter avere sia Kubernetes che un container runtime sul proprio pc. Permette inoltre una cosa molto utile, ovverò quella di disabilitare Kubernetes e lasciare attivo solo il container runtime.
Nel mio caso utilizzerò Docker Desktop su un computer Windows 11
Dopo aver installato Docker Desktop sarà possibile controllarne la versione andando su una finestra powershell e lanciare il comando “docker version”, la mia versione attuale come si può vedere dallo screenshot è la 24.0.2 sia per il Client che per il Server.
Con il comando docker ps sarà possibile vedere i container che sono in stato running, nel mio casò siccome non ho nessun contaiener avviato precedentemente il risultato del comando non mostrerà nulla.
A questo punto avviamo il nostro primo container lanciando: docker run hello-world
Come è possibile vedere dallo screenshot alcune cose sono successe dopo il comando, la prima cosa che è possibile vedere è come il docker client chieda al docker daemon di avviare il container. Il docker daemon a questo punto controlla se nella lista della immagini presente nel sistema operativo host esiste l’immagine con hello-world. Se non la trova localmente, andrà a ricercare questa immagine da un registry esterno che è hub.docker.com. Dopo aver scaricato l’immagine in locale il container viene avviato automaticamente mostrando il messaggio di “Hello from Docker!” e le informazioni di come il container è stato generato.
Da notare come nel nome dell’immagine che viene scaricata viene agganciato il tag “:latest” che ci fornisce le informazioni sulla versione dell’immagine. Questo tag può essere cambito per permetterci di scaricare immagini con una versione diversa dalla “latest”.
A questo punto verifichiamo come è possibile vedere sempre con il comando “docker ps” i container che sono running ma anche quelli che sono stati lanciati e dopo che hanno finito le loro operazioni sono stati stoppati.
docker ps -a
Come si può notare nello screenshot precedente docker ps non ci mostra tutti i container che sono in stato running e exited, per poterli vedere tutti ci basta aggiungere appunto il parametro “-a” e così possiamo vdere anche il container che abbiamo appena lanciato.
Un altro comando fondamentale di docker è “docker image ls” con cui possiamo vedere tutte le immagini scaricate sul nostro host.
A questo punto perchè non provare quello che ci ha suggerito il primo container che abbiamo lanciato, ovvero provare a lanciare un qualcosa di più ambizioso come un container ubuntu con cui interagire.
Il comando in questione è docker run -it ubuntu bash
-i = interactive mode
-t = tty
bash = è il comando che viene mandato al container
Il risultato sperato è quello qui sopra, ovvero l’avvio di un container ubuntu su cui possiamo interagire tramite bash e verificarne la versione tramite il semplice comando cat /etc/os-release
una volta verificato si potete uscire dal container usando il comando exit
Proviamo un altro container più ambizioso a questo punto, proviamo a un cotainer nginx che espone un servizio sulla porta container 80 ma che viene mappata sul nostro pc / porta host 8080.
docker run -d -p 8080:80
-d = detached, permette di mantenere running in backgroup il container riportandoci sul prompt dell’host
-p = associa una porta host:porta container
Possiamo verificare che il container è running con il comando docker ps
Per verificare che tutto funzioni correttamente potete usare il browser locale del vostro pc e puntare all’indirizzo localhost:8080
Se vogliamo avviare una versione particolare di quel container, possiamo utilizzare il TAG che ne identifica la versione per esempio: docker run redis:4.0
Ecco quel tag finale dopo il nome del container ci viene in aiuto in questi casi in cui vogliamo avviare una versione precedente rispetto all’ultima disponibile, se usiamo il comando RUN senza specificare i tag il risultato è che avvieremo come versione la “latest”.
Per poter eliminare un container va prima spento usando il comando “docker stop “id container”, l’id container è visualizzabile come output del comando run, ma è possibile comunque recupearlo facnedo sempre docker ps.
Per poter cancellare le immagini salvate nel container host, è necessario eliminare i container ad esso associate, utilizzando il comando docker ps -a e eliminandole con “docker rm id-cotainer”, come mostrato qui sotto.
Una volta eliminati i container è possibile eliminare anche l’immagine, controllando prima le immagini locali con docker image ls e poi successivamente con docker image rm “nome immagine” eliminarle come visibile nello screenshot poco sopra.
Per eliminare tutte le immagini in un colpo solo utilizzare il comando:
docker rmi $(docker images -aq)
CONTAINER STORAGE
I container per loro stessa natura non sono un oggetto in grado di mantenere dei dati persistenti dopo che sono stati spenti e riaccesi. C’è però un modo per poter creare un volume sia in precedenza che durante la creazione del container così da avere uno spazio persistente dove salvare i dati.
Nel seguente esempio creerò un volume tramite la command line di docker e poi aggiungerò questo volume al container che verrà creato.
Per creare il volume utilizzerò il semplice comando docker volume create gdvol1 e poi docker volume ls per verificare che sia stato correttamente creato.
A questo punto proverò ad avviare un container ubuntu a cui andrò ad agganciare il volume precedentemente creato sul path /demo del container.
docker run -it -v gdvol1:/demo ubuntu bash
Una volta che il container sarà attivo andrò a verificare tramite il comando df -h se il volume è stato correttamente collegato.
Come possiamo vedere poco sopra è possibile agganciare un volume persistente che non viene cancellato anche quando un container viene rimosso/cancellato.
: aufs, containers, image e volumes.
In questo paragrafo voglio parlare di volumi e di come è possibile montare dei volume ad un container uitlizzando i due metodi principali.
Prima di tutto è possibile creare un docker volume utilizzando il seguente comando:
- docker volume create data_volume
Il comando scritto sopra andrà a creare all’interno di /var/lib/docker/volumes la folder data_volume che sarà poi utilizzabile dai container come persistent storage.
Il comando seguente sarà poi quello di avviare un container nel mio esempio un container mysql usnado il comando -v che necessità di due valori, il docker volume e il path di riferimento all’interno del container, come nell’esempio qua sotto:
- docker run -v data_volume:/var/lib/mysql mysql
Cosi facendo avremo creato un container con un volume persistente, ed i dati verranno salvati appunto nel path che abbiamo visto qualche riga sopra.
E’ possibile però creare direttamente il docker volume all’avvio del container utilizzando sempre il comando -v, specificando una folder non esistente docker farà in modo di creare il docker volume specificato.
Un ulteriore possibilità è quella di mappare un volume che non è all’interno del path di default di docker, in questo caso basterà inserire come prima variabile il path che vogliamo che sia utilizzato come volume dal container, per esempio:
- docker run -v /data/mysql:/var/lib/mysql mysql
Il comando -v è un comando old style, il nuovo metodo è l’utilizzo del comando –mount che utilizza un metodo diverso, dove venongo specificate le variabili seguite da un = ed il valore/path.
docker run \ –mount type=bind,source=/data/mysql,target=/var/lib/mysql mysql
CONTAINER NETWORKING
Un altra parte fondamentale da capire per poter utilizzare al meglio i containers è come gestire la parte di rete. Alcuni network sono già disponibili già dalla prima installazione di docker, utilizzando il comando “docker networks ls” infatti saremo in grado di vedere i bridge networks, host networks e possibilimente anche altri. Il Bridge network è il network di default utilizzato dai nostri container se non diversamente indicato e solitamente utilizza il seguente CIDR 172.17.0.0/16. La particolarità del bridge network è che di base permette la comunicazione tra i container che fanno parte di questa rete mentre invece container in sparsi in diversi network bridge non possono comunicare tra loro. Host network invece come lo dice il nome permette ai container di utilizzare la rete dell’host in cui sono ospitati.
La cosa interessante da sapere per quanto riguarda il default bridge network è che di base i container come detto prima possono “pingarsi” tramite l’IP senza problemi, ma ciò non è vero se invece usiamo l’hostname. Per permettere di comunicare anche tramite hostname e quindi risoluzione nome/ip la possibilità è quella di creare un proprio bridge network. Sul nuovo bridge network creato infatti viene abilitata quella che si chiama Automatic Service Discovery, che permette appunto di poter comunicare tra i container tramite il nome. Ma vediamolo in azione creando subito un nuovo bridge network con il comando “docker network create giovanni-network“
E subito dopo controllarne le specifiche con il comando “docker network inspect giovanni-network | more“
Da notare come a questo nuovo bridge network è stato assegnato un altro indirizzamento a livello di CIDR: 172.18.0.0/16, questo dovrebbe permettere se proviamo a creare due container in questa network di potersi raggiungere tramite l’hostname. Proviamolo, lanciamo i seguenti comandi nella finestra powershell già aperta:
docker run -it -h ubuntu1 –network giovanni-network ubuntu bash
apt-get update
apt-get install iputils-ping -y
apt-get install iproute2 -y
A questo punto verifichiamo l’indirizzo IP con il comando “ip a“
A questo punto apriamo un altra finestra di powershell, e lanciamo i comandi fatti precedentemente con l’unica differenza che il nome host sarà ubuntu2
A questo punto possiamo provare il ping tra questi due container, sappiamo che ubuntu1 ha come indirizzo 172.18.0.2 mentre ubuntu2 172.18.0.3, proviamo sia con l’indirizzo ip che con il nome il risultato dovrebbe essere che in entrambi i casi i container riescano a comunicare tramite ping correttamente!
L’ultima prova è cercare di comunicare tramite ping tra container su bridge network diversi, ho deployato un terzo container ubuntu sul default bridge network e ho provato a fare ping verso i container precedentemente creati, ovviamente in questo caso la comunicazione non va a buon fine, poichè container in bridge network diversi non possono comunicare.
Per poter andare a fondo sulla questione bridge network e comunicazione tra i container bisognerebbe parlare di Kubernetes e del CNI – Container Network interface, argomento che vedrò di affrontare nel relativo articolo dedicato a Kubernetes.
Nel caso vi sia la necessità di creare altri network si può anche utilizzare lo stesso comando specificando il tipo di driver e la subnet:
docker network create \ –driver bridge \ –subnet 182.18.0.0/16 custom-isolated-network
CREARE UN CONTAINER CON DOCKER BUILD
La vera forza dei container è appunto la portabilità, e in questo caso significa che uno sviluppatore può creare qualcosa che può essere avviato dappertutto senza particolari problemi, a patto ovviamente di avere un container runtime che sia in grado di gestire l’immagine creata.
Cosa server quindi per creare un container da zero? Nel caso di docker quello che ci serve viene chiamato Dockerfile.
Dockerfile consiste in un gruppo di istruzioni standard come ad esempio: “FROM”, “CMD”, “COPY”, “RUN” per citarne alcune.
Per poter costruire il nostro container dobbiamo partire dai “Layer” o Livelli con cui viene costruito, per esempio il primo layer è il sistema operativo “ubuntu” seguito da un altro livello che è “apache” seguite poi dalla eventuale configurazione di apache.
Nel seguente esempio andremo a costruire il nostro Dockerfile con i suoi layer che poi verranno utilizzati per creare la struttura del container che verrà avviato.
Come menzionato sopra il container che andremo a creare utilizzerà apache2 web server. Questo servizio andrà a creare localmente sul container il file della pagina web chiamato appunto index.html. Quello che andremo a fare è invece creare un “nostro” index.html file modificato che verrà poi inserito nel container al posto dell’originale nel percorso /var/www/html.
Apache inoltre andrà a pubblicare questa pagina tramite la porta 80 che l’host dovrà poi reindirizzare per permetterci di raggiungere appunto il container. Ma partiamo per gradi, iniziamo a creare i file necessari per la creazione del container.
Usando powershell iniziamo a creare la cartella apache2 dove all’interno andreamo a creare i file necessari per il build del container:
– mkdir apache2
– cd apache2
– new-item Dockerfile
– new-item index.html
Dopo questi comandi (fatti nel mio caso fatti all’interno di powershell), vado a personalizzare i file iniziando dall’index.html
<html>
<head>
<title> La mia prima app in container! </title>
</head>
<body>
<p> Questo spazio web viene gestito da un container!!! </p>
</body>
</html>
A questo punto andiamo a costruire il nostro Dockerfile
FROM ubuntu:latest
RUN apt-get update; \
apt-get install apache2 -y; \
mv /var/www/html/index.html /var/www/html/index.html.orig
COPY index.html /var/www/html
EXPOSE 80
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
Rivediamo un secondo quanto scritto sopra per chiarezza. La prima riga FROM contiene le informazioni relative a quale immagine esistente utilizzare per creare la nostra container image. La seconda linea come si può vedere lancia diversi comandi, come “update”, “install” ed inoltre fa il move del file originale index.html rinominandolo così da permetterci nella riga successiva di andare a sostituirlo con quello presente nella cartella dove risiede anche il dockerfile.
La direttiva EXPOSE dice al web server di pubblicare il suo servizio web con la porta 80 e l’ultima riga usa il comando ENTRYPOINT per dare il comando al container di avviare esclusivamente il servizio apache2ctl in modalità foreground.
Arrivati a questo punto se è tutto correttamente preparato non ci rimane altro che creare la nostra container image utilizzando il comando “docker build“
docker build -t giovanni/apache2 .
docker images
docker history giovanni/apache2
Finalmente dopo queste verifiche possiamo avviare il nostro container:
docker run -h webserver.domain.com -d -p 8080:80 giovanni/apache2
Test localhost:8080 dal browser del mio pc
verifica dell’hostname tramite docker exec
docker exec -ti 5b7fcad2ac0d hostname ( l’id del container è stato preso tramite il comando docker ps)
Verifica del file index.html copiato all’interno del container utilizzando bash nel comando docker exec
docker exec -it 5b7fcad2ac0d bash
cd /var/www/html
cat index.html
Se siete arrivati fino a qui CONGRATULAZIONI avete creato il vostro primo container!!!
Docker Registry
Quando andiamo ad avviare un container usando il semplice comando “docker run nginx” in realtà stiamo facendo riferimento ad un immagine che è nominata nginx e viene automaticamente cercata nel repository delle immagini pubblico dockerhub.
nome immagine : nginx ed in questo caso siccome non abbiamo specificato nulla davanti, significa che anche l’utente registry a cui si fa riferimento è lo stesso del nome immagine:
nginx/nginx = la prima parte sta per il nome dell’utente e la seconda parte il nome dell’immagine
in realtà il path completo è il seguente: docker.io/nginx/nginx che viene utilizzato come repository quando viene appunto eseguito il comando run.
Ma ci sono anche altri registry utilizzabili pubblicamente come per esempio:
gcr.io/kubernetes-e2e-test-images/dnsutils
docker login = viene utilizzato come comando per accedere tramite utente/password ad un private registry (che è sempre buona norma utilizzare e non pubblicare a tutti)
un esempio di avvio di un container da private registry è il seguente:
docker run private-registry.io/apps/internal-app
Avvio di un registry locale usando docker:
docker run -d -p 5000:5000 –name registry registry:2
tagging dell’immagine in maniera tale da essere salvata sul registry locale
docker image tag my-image localhost:5000/my-image
push dell’immagine sul registry locale
docker push localhost:5000/my-image
pull dell’immagine dal registry locale usando localhost
docker pull localhost:5000/my-image
pull dell’immagine puntando all’ip
docker pull 192.168.56.100:5000/my-image
Grazie mille, Giovanni. Con questa semplice guida ho capito il concetto di container, ne ero completamentre a digiuno, grazie!