Introduzione

Stormforce

Docker è una piattaforma software che permette lo sviluppo, il test e il deployment di applicativi in modo veloce. Docker impscchetta il software in unità standard chiamate contenitori che forniscono tutto ciò di cui l'applicativo ha bisogno, incluso librerie, strumenti di sistema, codice e ambiente runtime-

Il presente corso esamina i fondamenti della tecnologia Docker, con ogni aspetto esemplificato da esercizi pratici e nell'ottica di comprensione del funzionamento dei metodi usati.

Lo scopo è di rendere il partecipante sufficientemente edotto delle basi di Docker da permettere un approfondimento individuale successivo.

Vengono coperti i seguenti aspetti fondamentali:

  • gestione di immagini e di contenitori di processo, di rete e di dati
  • inserimento di un proprio applicativo in un'immagine Docker
  • orchestrazione locale di più contenitori con Docker Compose

Ultimo aggiornamento: Febbraio 2024

Licenza

Gfdl

La presente documentazione è distribuita secondo i termini della GNU Free Documentation License Versione 1.3, 3 novembre 200t8.

Ne è garantita assoluta libertà di copia, ridistribuzione e modifica, sia a scopi commerciali che non.

L'autore detiene il credito per il lavoro originale ma nè credito nè responsabilità per le modifiche.

Qualsiasi lavoro derivato deve essere conforme a questa stessa licenza.

Il testo pieno della licenza è a:

https://www.gnu.org/licenses/fdl.txt

Architettura

Dockarch

Occorre comprendere il motivo dell'invenzione e rapida adozione di Docker, e quali vantaggi da.

Quindi si procede ad un overview della sua architettura.

Segue la procedura di installazione di Docker, soprattutto su piattaforma Linux.

Finalmente viene data la sintassi del comando client docker, e la struttura dei suoi sottocomandi.

Evoluzione e componenti

Solomon

Docker è un ambiente di isolamento delle risorse necessarie ad un applicativo all'interno di un contenitore

  • Da circa 2013, ideato da Solomon Hykes
  • Contributo di Red Hat
  • Richiede Linux a 64 bit Kernel 2.6+
  • Licenza Apache (open)
  • Scritto in linguaggio Go
  • Virtualizzazione entro il sistema operativo
  • Direzione: software indipendente dal sistema operativo

Macchine Virtuali e Contenitori

Macchine Virtuali: Virtualizzazione dell'Hardware

  • Più sistemi operativi diversi
  • Più flessibile
  • Più maturo

Macchine Virtuali

Contenitori: Virtualizzazione dell'Ambiente Operativo

  • Più efficienza
  • Meno manutenzione di sistema
  • Partenza molto più veloce
  • Migliaia di immagini disponibili, con documentazione

Contenitori

NB: Il Kernel è quello dello host Linux

Tipi di Contenitori

  • Process container - l’ambiente di esecuzione di un processo principale e possibilmente altri processi
  • Network container - fornisce connettività, indirizzamento e risoluzione nomi ai contenitori di processo
  • Data container - mantiene i dati in volumi e li persiste quando i contenitori di processo non sono attivi

Tutte le immagini da cui derivano i contenitori sono mantenuti in un repository sul computer locale

Tipi di contenitori

Docker

Docker è scritto nel linguaggio di programmazione Go

Molti dei suoi comportamenti sono dovuti al linguaggio Go

E’ distribuito da docker.com (o docker.io)

Ne esistono due versioni:

  • enterprise - a sottoscrizione di licenza
  • community - gratuito ed Open Source

Docker si basa su features architettoniche di Linux:

  • cgroups - supporto kernel all’isolamento processi
  • Union File System - FS composto da layers

La docker.com supporta il prodotto solo su Linux

Il Linux di riferimento è Ubuntu

E’ in fase di sviluppo un Linux di riferimento specifico per Docker indipendente dalle distribuzioni correnti

Immagini

Un contenitore è l'istanza run-time di un'immagine.

Un'immagine è la combinazione di un certo numero, variabile a seconda della specifica immagine, di strati software (Layers), integrati all'interno di uno Union File System.

UFS

Cgroups

Un cgroup (control group) è una feature che limita, traccia ed isola l’uso di risorse di una collezione di processi.

I Cgroups sono una parte integrale del kernel Linux

Forniscono l’implementazione nel system space dei contenitori dello user space

Cgroups

Componenti di Docker

  • Dockerfile: specifica dei componenti necessari. File testuale di specifiche
  • Image: risultato della compilazione del docker file. Serie di layers in un repository locale o remoto
  • Container: istanza di realizzazione di una image

Più containers della stessa image sono istanze separate e indipendenti

Comp

Client-Server

Docker segue l'architettura Client-Server. Vi sono un eseguinile client e due server.

docker - client, Command Line Interface. Interfaccia con l’utente tramite comandi shell

dockerd - high-level server, interagisce con il client. Implementa il comportamento di Docker

containerd - low-level server, gestisce i container. Usato anche con contenitori non Docker, p.es. Kubernetes

Cliser

Il server è gestito come servizio standard di Linux e si può controllare con il comando amministrativo:

sudo systemctl azione docker

ove azione può essere:

  • status - stato del servizio e processi attivi
  • start - far partire il servizio
  • stop - fermare il servizio
  • enable - configurare lo start automatico al boot
  • disable - disabilitare lo start automatico al boot

Due componenti del server:

  • docker.service - il servizio
  • docker.socket - il socket Unix di collegamento

Il collegamento tra client e server avviene tramite un socket. In locale (default) questo è un socket Unix.

Socket

Installazione su Linux

Tipi di Installazione

Linux

Ubuntu e simili [deb], RedHat e simili (CentOS) [RPM]

  • Su hardware diretto (Docker Engine)
  • Su Macchina Virtuale Linux (Docker Engine)
    • VirtualBox (preferito)
    • KVM
  • Con Docker Desktop

Windows

  • Su Macchina Virtuale Linux (Docker Engine)
    • VirtualBox (preferito)
    • VMWare
  • Linux on Windows con WSL2 (Windows Subsystem for Linux)
  • Con Docker Desktop

Docker Desktop

Nuova offerta Docker. Basato su Macchina Virtuale Linux.

DDstack

  • Soluzione per principianti
    • Interfaccia grafica Dashboard comune
    • Molte meno opzioni che da linea di comando
  • Non ancora performante o stabile
    • Promesse di migliorie

DDifc

NB: Non si possono avere simultaneamente attivi Docker Engine e Docker Desktop

Su Linux, prima di Docker Desktop:

sudo systemctl stop docker
sudo systemctl disable docker.service
sudo systemctl disable docker.socket

Procedura di Installazione

Su Hardware o su VM VirtualBox, requisiti:

  • Linux Ubuntu o simile (p.es. Mint), recente, 64 bit
  • 4 GB RAM
  • 100+ GB HD
  • Connessione a Internet

Metodo 1

Installare dai pacchetti della distribuzione.

Facile, veloce e sicuramente allineato con il resto del software di distribuzione.

Ubuntu, Mint, ecc (DEB)

sudo apt update
sudo apt install docker.io

Però non è l'ultima versione disponibile.

Red Hat, CentOS, Oracle, ecc (RPM)

sudo dnf check-update

Configurare il repository di Docker:

sudo dnf install -y dnf-utils zip unzip
sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo

Installare Docker:

sudo dnf remove -y runc
sudo dnf install -y docker-ce --nobest

Abilitare e lanciare il servixio:

sudo systemctl enable docker
sudo systemctl start docker

Metodo 2

Esempio per le pacchettizzazioni DEB (Ubuntu, ...)

Installare dal repository di docker.io.

Versione più recente, e permette la successiva estensione con plugins.

Occorre configurare la locazione del repository ed scaricare il necessario certificato. Questo si compie al meglio, preparando la seguente procedura shell, docker-repo.sh:

#! /bin/bash
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$UBUNTU_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# Install the Docker packages:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Rendere la procedura eseguibile e lanciarla:

chmod +x docker-repo.sh
./docker-repo.sh

Questo da l'ultima versione stabile disponibile, incluso un numero di plugin che sono l'ultima evoluzione di Docker.

Dopo l'installazione

Solo i membri del gruppo docker possono usare l'ambiente.

Configurare l’utente corrente:

sudo usermod -aG docker $USER

Reboot del sistema. In teoria un relogin è sufficiente, ma la VM Ubuntu o RedHat vuole proprio un reboot.

Test dopo il reboot:

docker info

Ambienti e comandi

Il client docker è un applicativo da linea di comando (CLI) che:

  • interagisce con l’utente
  • invia i comandi al server per l’esecuzione

Nella versione di Docker più moderna i comandi hanno la struttura:

docker ambiente comando

Alcuni comandi storici non hanno bisogno della specifica dell’ambiente e sono del formato:

docker comando

Per avere un veloce aiuto sui comandi disponibili basta:

docker

Per avere un veloce aiuto sui comandi disponibili in un certo ambiente:

docker ambiente

docker ambiente --help - dà anche le opzioni

Principali Ambienti

Ogni ambiente permette la gestione di un diverso aspetto di Docker:

  • config - configurazioni del cluster Swarm
  • container – contenitori di processo
  • image - immagini
  • network – contenitori di reti
  • node - nodi del cluster Swarm
  • plugin - plugins
  • secret - segreti del cluster Swarm
  • service - servizi offerti dal cluster Swarm
  • swarm - il cluster Swarm
  • system - aspetti generali del sistema
  • trust - fiducia delle immagini
  • volume - contenitori di dati

Molti comandi legacy non richiedono la specifica dell’ambiente

Manualistica

Docker è dotato di ricca manualistica di riferimento Il nome della pagina di manuale mappa il comando docker dato:

docker ambiente comando

ha manualistica con

man docker-ambiente-comando

Esempi:

man docker
man docker-run
man docker-network-create

Immagini

Img01

Un'immagine Docker è un template read-only.

Contiene un insieme di istruzioni per la creazione di un container Docker.

E' una maniera conveniente di impacchettare un applicativo e l'ambiente operativo associato di cui l'applicativo ha bisogno.

Le immagini sono conservate in un Repository o Registry, ove si possono porre una volta sviluppate, e da li sono scaricate alla piattaforma ove vengono generati i contenitori.

Operazioni su immagini

Le immagini sono contenute in un Registry.

Il Registry di default è su docker.io.

Vera URL corrente (non serve quasi mai saperla): https://registry.hub.docker.com/v2/repositories/library

Il comando di gestione immagini è:

docker image sottocomando

I sottocomandi principali sono:

  • docker image pull immagine

oppure

  • docker pull immagine

compiono il download dell’immagine

--

  • docker image ls

oppure

  • docker images

lista le immagini locali

--

  • docker image rm IDimmagine

  • docker rmi IDimmagine

rimuove un’immagine locale. IDimmagine è lo hash dell’immagine

Esempi

Imgop1

Imgop2

Registry

URL con protocollo https, alla quale docker si collega per scaricare immagini, con APi definite.

Il registry di default è: https://registry.hub.docker.com/, o https//hub.docker.com/, detto Docker Hub.

Vi sono in rete altri registries, p.es.:

  • Quay (https://quay.io/)
  • Google Container Registry (https://cloud.google.com/container-registry/)
  • AWS Container Registry (https://aws.amazon.com/ecr/)

E' possibile collegarsi liberamente (al momento) a Docker Hub, navigare per avere informazioni sulle immagini disponibili, e scaricare le immagini.

Per compiere l’upload occorre registrarsi

Il collegamento ad altri registries può prevedere una sottoscrizione a pagamento, e permette l'upload di immagini prodotte localmente.

Ricerca di Immagine

Il comando è:

docker search [opzioni] immagine

Ricerca sul registry Docker Hub

Esempio:

docker search fedora

Tutte le immagini che contengono la stringa fedora

La lista ritornata è limitata alle prime 25 entries. Non si possono averne di più. Per averne di meno:

docker search --limit 4 fedora

Ogni immagine riceve stars - punti di valutazione. Limitare la ricerca ad un minimo di 3 stars:

docker search --filter=stars=3 fedora

Una delle immagini può essere ufficiale, le altre contribuite da persone registrate. Cercare solo l’immagine ufficiale;

docker search --filter=is-official=true fedora

Alcune immagini sono Automated Build: l’immagine viene ricompilata automaticamente al momento del download. Cercare le immagini non automated:

docker search --filter=is-automated=false fedora

Per avere una descrizione più completa

docker search --no-trunc fedora

Esempio

Search Image

Nomi delle immagini

Struttura del Nome

Il nome di un’immagine ha il formato

autore/immagine:tag

ove:

  • autore - è l’identificativo del produttore, registrato a docker.io e univoco in tale registry
  • immagine - il nome dell’immagine, un autore può produrre più immagini, ciascuna con nome univoco
  • tag - solitamente corrisponde alla versione

I seguenti componenti sono opzionali:

autore - se manca, si intende l’immagine ufficiale sancita da docker.io (massimo: una)

tag - se manca vale latest

Esempi:

debian

debian:9.0

s390x/debian

Ricerca Tag

Non esiste in docker search la funzionalità per conoscere i tag associati ad un’immagine. Però si può scrivere una procedura shell.

Occorre l’utility jq che è simile a sed ma funziona su dati JSON:

sudo apt install jq

La procedura shell per cercare I tags di debian è quindi, p.es. dockertags.sh:

while [ $? == 0 ]
do    
    i=$((i+1))    
    curl https://registry.hub.docker.com/v2/repositories/library/debian/tags/?page=$i \
    2>/dev/null|jq '."results"[]["name"]' 
done

Scarico di Immagine

Il comando per scaricare da Docker Hub è:

docker pull immagine[:tag]

Se il tag non è indicato è latest

C’è l’opzione -a per scaricare tutti i tag.

Non usarla!: impiega troppo tempo e spazio disco

Esempio:

docker pull debian:9.0

Per un pull da un altro registry, non Docker Hub:

docker pull registry-URL/immagine[:tag]

docker pull registry-ip:porta/immagine[:tag]

Nota:

In caso di interruzione del pull di un'immagine, ripetendo il comando di pull il download ricomincia dall'inizio. La versione corrente ancora non supporta il ripristino di download.

Lista delle Immagini

Per listare le immagini locali:

docker image ls

docker images

Opzioni:

  • -q - quiet - lista solo gli ID
  • -a - all - lista tutti i branches
  • --digest - include il digest (checksum) delle immagini
  • --no-trunc - mostra lo ID SHA per intero

Il chechsum SHA256 di un'immagine si chiama il suo Digest. Per aumentare la sicurezza si può includere nel comando di pull:

docker pull immagine:tag@digest

Per vedere i digest di un'immagine scaricata, p.es.:

docker images --digests alpine

Rimozione Immagini

Per rimuovere un'immagine:

docker rmi hash

P.es.:

docker rmi 820010c31e66

Viene rimossa l'immagine e tutti i suoi layers che non siano in condivisione con altre immagini.

Per (eventualmente) rimuovere tutte le immagini:

docker rmi -f $(docker images -q)

Attenzione che le immagini rimosse non sono più recuperabili se non riscaricandole dal registry

Salvataggio di Immagini

Per l’esercizio scaricare prima l’immagine golang:

docker pull golang

E’ possibile salvare un’immagine in un file tar, per eventuale asporto:

docker image save -o file.tar immagine

P.es.:

docker image save -o golang.tar golang

Senza l’opzione -o scrive su standard output. Questo può essere utile quando si vuole comprimere un’immagine. p.es.:

docker image save golang | gzip - > golang.tgz

Recupero di Immagini

Per recuperare un’immagine da un file tar:

docker image load -i file.tar

P.es.:

docker image load -i golang.tar

Senza l’opzione -i legge da standard input. Se l’immagine è compressa:

gunzip -c golang.tgz | docker image load

Dentro un’immagine ci sono tutti i suoi attributi. Questi vengono salvati e recuperati da save e load.

Si può ispezionare l’archivio tar con:

tar tvf golang.tar

Registry locale

Anzichè il registry Docker Hub si può usare un registry locale.

Un registry locale può venire fornito da un container:

docker run -d -p 5000:5000 --restart=always \
--name registry -v ~/.docker/registry:/var/lib/registry \
registry

Per salvare un’immagine al registry locale occorre prima taggarla:

docker tag ubuntu:20.04 localhost:5000/myubuntu

Ora si può salvare:

docker push localhost:5000/myubuntu

Proviamo a cancellarla e recuperarla:

docker rmi localhost:5000/myubuntu
docker pull localhost:5000/myubuntu

Lista del registry locale alla URL:

curl -X GET http://localhost:5000/v2/_catalog

L'immagine registry fornita dal Docker Hub non è molto sofisticata.

Ha i comandi di pull e push, ma mancano altri comandi di gestione amministrativa del registry.

La sua documentazione è su https://hub.docker.com/_/registry.

In alternativa è possibile usare il GitHub Container Registry se si possiede un account GitHub.

Contenitori di Processo

Dockcont

Un contenitore di processo è l’istanza di un’immagine.

Parallelo: file eseguibile → processo

Comando di generazione:

docker run [-ti|-d] [opzioni] immagine [comando]

Un comando di default può esistere nel contenitore.

Due tipi di contenitori:

  • Interattivi

docker run -ti [opzioni] immagine [comando]

-ti - terminal interactive

Si interagisce tramite terminale col comando del contenitore- All’uscita il contenitore termina.

  • Non interattivi

docker run -d [opzioni] immagine [comando]

-d - detached, in background

Altrimenti il pronto non torna.

Contenitori interattivi

Esempio:

docker run -ti alpine:3.7

Il comando di default qui è /bin/sh. Come risultato siamo dentro il sistema operativo Alpine. Tutti i comandi che diamo sono quelli di Alpine.

Uscita temporanea:

Ctrl-P Ctrl-Q

Torna il pronto dello host Linux

Ispezione dei contenitori attivi:

docker ps

Notare lo id (hash) e il nome di fantasia assegnato d’ufficio al contenitore

Ritorno nel contenitore:

docker attach ID

oppure

docker attach nome

Uscita finale, da Alpine:

exit

Torna il pronto dello host linux. Il contenitore è terminato.

Lista contenitori attivi:

docker ps

Lista di tutti i contenitori, anche terminati

docker ps -a

I contenitori terminati vanno rimossi:

docker rm ID

oppure

docker rm nome

Eventuale rimozione di tutti i contenitori:

docker rm -f $(docker ps -aq)

Attenzione : si possono perdere dei dati importanti

Alpine Linux

Alpine Logo

La release Linux di riferimento per Docker è Ubuntu. Questa è anche l’immagine di base su cui molte immagini di applicativi sono costruite. Ma: richiede molti MB di disco.

Un’alternativa recente per lo sviluppo immagini di applicativi è Alpine Linux

  • Molto ridotto nel footprint
  • Richiede pochissimi MB di disco

Attenzioni:

  • Alpine ha comandi di amministrazione diversi
  • I comandi di base sono ottenuti con busybox
  • L’ambiente runtime è MUSL e non GLIBC

Questo necessita di attenzioni quando si sviluppano eseguibili compilati dai contenitori

Contenitori detached

Contenitore interattivo

Sullo host scrivere la procedura shell forever.sh:

i=0
while true
do
  sleep 2
  i=`expr $i + 1`
  echo $i
done

Lanciare un contenitore che esegue questa procedura:

docker run -ti alpine /bin/sh -c "$(cat forever.sh)"

L’output viene sul video del Linux host

Fermarlo: Ctrl-C

Verificare con docker ps -a che il contenitore è fermo

Test: perchè il seguente non funziona?

docker run alpine:3.7 /bin/sh forever.sh

Contenitore non interattivo

Lanciare un contenitore non interattivo che esegue la procedura:

docker run -d alpine /bin/sh -c "$(cat forever.sh)"

Il pronto torna subito, il contenitore è detached

Per vedere l’output:

docker logs IDalpine

Se ci si collega ad un contenitore detached:

docker attach IDalpine

Questo non è più detached e il suo output arriva sul terminale Linux host.

Quando si preme Ctrl-C torna il pronto host ma il contenitore è fermato.

Se invece si preme Ctrl-P Ctrl-Q torna il ptonto host e il contenitore non è fermato.

Rimuovere tutti i contenitori:

docker rm [-f] $(docker ps -qa)

-f - anche i contenitori non fermi

Cont States

Operazioni sui contenitori

Pausa di Contenitore

Lanciare un contenitore non interattivo che esegue la nostra procedura:

docker run -d alpine /bin/sh -c "$(cat forever.sh)"

Ispezionare i logs:

docker ps - per sapere il nome

docker logs nome

Mettere in pausa il contenitore:

docker pause nome

docker ps

docker logs nome

Togliere la pausa dopo un po’:

docker unpause nome

docker ps

docker logs nome

Il comando docker pause non termina i contenitori ma li sospende temporaneamente

Fermare un Contenitore

Fermare il contenitore:

docker stop nome

docker ps

docker ps -a

docker logs nome

Riavviare il contenitore:

docker start nome

docker ps

docker logs

Un contenitore fermato e riavviato ricomincia dall’inizio

Al termine dell’esercizio fermare e rimuovere tutti i contenitori

Pause

Altre Operazioni

Forzare la rimozione di un container attivo:

docker rm -f ID

Lanciare un container in modalità 'detached':

docker run -d ubuntu:20.04 sleep 10000

Il container deve fare qualcosa

Eseguire un comando su un container attivo:

docker exec -ti ID_ubuntu bash

Il termine del comando shell (Ctrl-D) non ferma il container

Opzioni di Run

Il comando docker run ha numerosissime opzioni, alcune tra le più usate sono:

--rm - rimuove automaticamente il contenitore quando viene fermato

--name nome - assegna il nome indicato e non quello fantasia

Esempi:

docker run --rm -ti --name alp alpine:3.7 /bin/sh
  • Lancia il contenitore col nome alp
  • Rimuove il contenitore quando si dà il comando exit
  • All’atto della rimozione di un contenitore vengono rimossi anche i suoi logs

Personalizzazioni

Formato del rapporto:

docker ps -a --format $FORMAT

ove:

export FORMAT="\nID\t{{.ID}}\nIMAGE\t{{.Image}}\
\nCOMMAND\t{{.Command}}\nCREATED\t{{.RunningFor}}\
\nSTATUS\t{{.Status}}\nPORTS\t{{.Ports}}\nNAMES\t{{.Names}}\n"

Anche:

alias dkp='docker ps -a --format $FORMAT'
alias dki='docker images'
alias dkr='docker rm'

Da inserirsi in .profile

Modifica di immagini

Lanciare uno nginx con un nome assegnato:

docker run -d --name my_nginxtemp1 nginx
docker ps

Ispezionare il contenitore e trovarne l'indirizzo IP assegnato:

docker inspect my_nginxtemp1 | grep IPAddress

Aprire il browser all'indirizzo IP indicato

Connettersi al container con una shell:

docker exec -i -t my_nginxtemp1 bash

Abbiamo una shell di root. Verificare la Document Root del server web:

cd /usr/share/nginx/html
ls

Sostituire index.html:

echo '<h1>CUSTOMIZED!</h1>' > index.html

Verificare col browser.

Uscire dal container:

exit

Creare una copia del container corrente come nuova immagine:

docker commit my_nginxtemp1 my_nginx1

Verificare:

docker images

Rimuovere il vecchio container e lanciarne uno nuovo con la nuova immagine:

docker rm -f my_nginxtemp1
docker run -d --name nuova1 my_nginx1

Verificare l'indirizzo IP:

docker inspect nuova1 | grep IPAddress

Verificare col browser all’indirizzo IP trovato.

Contenitori di Dati

Dockervol

I contenitori lavorano su dati. Sorge il problema che i dati elaborati devono rimanere, persistere, anche quando i contenitori terminano di operare.

Questo problema è stato originariamente risolto compiendo un mappaggio tra dati sul contenitore e sulla macchina ospitante. La modifica di uno dei due lati del mappaggio è rispecchiata nella stessa modifica sull'altro lato.

Nella versione originale di Docker v'era solo il concetto di contenitori, corrispondente a quelli che ora chiamiamo contenitori di processo. Nelle versioni successive è avvenuta una differenziazione di tipi diversi di contenitori.

Sono stati introdotti i Contenitori di Dati, anche detti Volumi, che hanno un'esistenza indipendente dai Contenitori di Processo.

Docker si preoccupa quindi nativamente anche della gestione dei dati dell'applicativo. Altri ambienti simili, quali Kubernetes, non lo fanno e si basano su soluzioni esterne.

Mappaggio a host

Persistere i Dati

Quando un container è rimosso i suoi dati sono persi

Soluzione: mappare una directory dello host ad una directory del container, con l’opzione di lancio:

-v dir_host:dir_container

Mappaggio detto Host Mapping

  • Si può usare l’opzione più volte, per più directories
  • Si può effettuare solo al lancio del container

Host Mapping

Dati Persistenti

Download di un web server:

docker pull nginx

Creare una directory web1 con il file index.html:

mkdir -p ~/docker/web1; cd ~/docker/web1; pwd

Editare il file index.utml:

<html><body>
<h1>FUNZIONA</h1>
</body></html>```

Lanciare un container che mappa la directory:

docker run -d -v ~/docker/web1:/usr/share/nginx/html nginx

Deve essere un percorso assoluto sullo host. Sul container può essere relativo alla dir di default

Verificare l'indirizzo IP del container:

docker inspect ID | grep IPAddress

Provare il collegamento via browser all’indirizzo IP

Condivisione di file

Si può condividere un singolo file:

cd
touch  ~/docker/example.txt

Il file deve esistere prima di condividerlo al container o docker pensa che sia una directory e la crea

Da host:

cd
docker run --rm -ti --name sharing -v $PWD/docker/example.txt:/tmp/example ubuntu:14.04 bash

E nel contenitore:

echo "hello" > /tmp/example
exit

Ritornati sullo host:

cat ~/docker/example.txt

Note

I permessi di default del container sono di root

  • Se una directory viene creata sullo host ha i permessi di root
  • Altrimenti crearla manualmente prima del mappaggio
  • Sempre attenzione ai permessi dei dati mappati

Volumi

Contenitori di Dati

I Contenitori di Dati sono formalmente chiamati Volumi.

  • Sono interamente contenuti nel repository locale
  • Non sono mappaggi al file system host

Due tipi:

  • Volume Persistente - continua ad esistere anche dopo la rimozione dei contenitori processo che lo usano

  • Volume Effimero - cessa di esistere con la rimozione dei contenitori che lo usano

Volume Persistente

VolCond

Web Counter App

Costruiamo un'immagine che scrive alla webdir di nginx:

mkdir ~/docker/webcounter
cd ~/docker/webcounter

Editiamo il file webcounter.sh:

#! /bin/bash
x=0
while true
do
  x=$[x + 1]
  echo "<br/><h1>$x</h1>" > /usr/share/nginx/html/index.html
  sleep 1 
done

Costruiamo il file Dockerfile di specifica di una nuova immagine. Maggiori spiegazioni seguiranno.

FROM ubuntu
COPY webcounter.sh /usr/local/bin/webcounter.sh
RUN chmod +x /usr/local/bin/webcounter.sh
CMD ["/usr/local/bin/webcounter.sh"]

Con il Dockerfile costruiamo la nuova immagine my_webapp1:

docker build -t my_webapp1 .

Condivisione tramite volume

Creiamo il volume persistente:

docker volume create myvol

Non è visibile con docker ps -a ma con docker volume ls

Creiamo il contenitore di processi nginx che mappa il volume persistente alla sua directory di pagine statiche:

docker run -d --name web_server -v myvol:/usr/share/nginx/html -p 8080:80 nginx

Creiamo il contenitore di processi con l’applicativo, che prende il volume persistente da quello già mappato dal server:

docker run -d --name web_app --volumes-from web_server my_webapp1

Verifichiamo col browser a localhost:8080

Verifica della persistenza

Fermiamo e rimuoviamo i contenitori di processi:

docker stop web_server web_app
docker rm web_server web_app

Il contenitore dati esiste ancora:

docker volume ls

Rilanciamo un web_server rimappando il contenitore dati:

docker run -d --name web_server -v myvol:/usr/share/nginx/html -p 8080:80 nginx

Verifichiamo con il browser a localhost:8080

Nota

Questo non funziona:

Salvare il contenuto del contenitore dati - tentativo

Generare un’immagine

docker container commit web_server web_server_backup

Ora la si può salvare ad un file tar

docker image save -o web_server_backup.tar web_server_backup

Il commit di un contenitore dati non include i volumi dati montati

Volume Effimero

Volume effimero: scompare quando i container che lo usano sono terminati

Container che crea un volume:

docker run -ti --rm --name one -v /shared_data ubuntu bash

Container che usa un volume:

docker run -ti --rm --name two --volumes-from one ubuntu bash

Su two:

touch /shared_data/example

Su one:

ls /shared_data

E' visibile example

Terminare one. Su two è ancora visibile example.

Il volume mantenuto dal server docker

Se rilanciamo one con la stessa condivisione dati, two non lo vede più

Istanze diverse di volume condiviso

Backup e restore di volumi

Backup di Volume

Backup di un volume condiviso in un file tar

Creare la directory che ospiterà il tar:

mkdir backup

Lanciare un contenitore temporaneo:

docker run -d --rm \
  -v myvol:/usr/share/nginx/html \
  -v $PWD/backup:/backup ubuntu \
  bash -c "cd /usr/share/nginx/html \
           && tar cvf /backup/myvol.tar ."

Si monta il volume

-v myvol:/usr/share/nginx/html

Si mappa la directory in cui viene prodotto il tar

-v $PWD/backup:/backup

Non si può usare ./backup perchè deve essere un percorso assoluto

Si esegue una shell bash passando il comando che va nella directory coi dati presi dal volume condiviso

cd /usr/share/nginx/html

ed esegue il tar

tar cvf /backup/myvol.tar .

Al termine del comando il contenitore termina e viene rimosso

Restore di Volume

Restore da un file tar

Creazione di un volume persistente:

docker volume create myvol1

Un contenitore temporaneo che compie il restore:

docker run -d --rm \
  -v myvol1:/usr/share/nginx/html \
  -v $PWD/backup:/backup ubuntu \
  bash -c "cd /usr/share/nginx/html \
           && tar xvf /backup/myvol.tar"

Il file è estratto e va anche nel volume myvol1

Il contenitore ubuntu termina ma il volume dati persiste

Verifica: lanciare un contenitore che usa il volume dati:

docker run -d --name web_server -v myvol1:/usr/share/nginx/html -p 8080:80 nginx

Verificare col browser a localhost:8080

Utility esterna

Scaricare l’immagine:

docker pull loomchild/volume-backup

Esempio di backup di volume:

docker run -v myvol:/volume --rm loomchild/volume-backup backup - > myvol.tar.bz2
  • Il file è compresso con bzip2
  • Il volume non deve al momento essere in uso da nessun contenitore processo

Esempio di restore di volume

Creare un volume destinazione, deve essere vuoto:

docker volume create myvol1

Eseguire il restore:

cat myvol.tar.bz2 | docker run -i -v myvol1:/volume --rm loomchild/volume-backup restore -

Verificare con:

docker run -d --name web_server -v myvol1:/usr/share/nginx/html -p 8080:80 nginx

docker prune

Rimuove oggetti di docker che non sono al momento in uso da parte di nessun contenitore di processo

  • Non vi è nessun contenitore processi attivo che in questo momento usa l’oggetto
  • Chiede conferma

Può essere molto pericoloso

  • docker volume prune - rimuove i volumi persistenti non in uso

Meglio usare: docker volume rm nome - il solo volume col nome

  • docker network prune - rimuove le reti non in uso

Meglio usare: docker network rm nome - la sola rete col nome

  • docker image prune - rimuove le immagini non in uso

Meglio usare: docker image rm nome - la sola immagine col nome

Contenitori di Reti

Docknet

Un Contenitore di Rete è un oggetto Docker che implementa un Software Defined Network (SDN).

Ciò è ottenuto modificando automaticamente le tabelle di routing e NAT (Network Address Translation) nell'ambiente iptables di Linux.

Forwarding

Perchè le reti Docker funzionino è necessario che il forwarding (parametro kernel) di Linux sia abilitato.

Di solito lo è di default.

Proviamo a disabilitarlo:

sudo sysctl -w net.ipv4.ip_forward=0

Ora lanciamo un container:

docker run --ti --rm --name web1 -d alpine sh

Otteniamo l'avvertimento:

WARNING: IPv4 forwarding is disabled. Networking will not work.

Il container parte lo stesso, e ci troviamo nella sua shell. Ma non potremo eseguire comandi di rete.

Usciamo con exit. Il container è rimosso automaticamente poichè l'avevamo lanciato con l'opzione --rm.

Riabilitiamo il forwarding:

sudo sysctl -w net.ipv4.ip_forward=1

Dispositivi di rete

Rete di Base

Basenet

Ad ogni container viene assegnata un’interfaccia sulla rete interna di default di Docker, 172.17.0.0/16, con un indirizzo IP in sequenza

Due containers diversi si vedono. Da macchina host:

docker run -ti --name one alpine sh

Dal container one:

ip a → IP: 172.17.0.2

Ctrl-P Ctrl-Q - per uscire senza fermare il contenitore

Da macchina host:

docker run -ti --name two alpine sh

Dal container two:

ip a → IP: 172.17.0.3

ping 172.17.0.2 → funziona

ma

ping one → non funziona

Non vi è un ambiente di risoluzione nomi indirizzi sulla rete interna di default

Port publishing

Se in un'immagine si trova un applicativo che apre una porta di rete, l'immagine espone (expose) tale porta di rete.

Quando dallo host lanciamo un container di quell'immagine occorre pubblicare (publish) tale porta di rete.

Con il comando ip scopriamo l'indirizzo IP della macchina host e chiamiamolo IPH.

Scarichiamo un'immagine che espone la porta 8080 e lanciamone un container.

NB

  • Il fatto che l'immagine scaricata apra la porta 8080 non è conoscenza innata, ma ricavato dalla documentazione che l'autore dell'immagine ha fornito. Nel Dockerfile (cf. ultra) di quell'immagine l'autore aveva specificato il parametro EXPOSE 8080.
  • Inoltre il programma eseguibile -- in questo caso un web server -- che era stato inserito nell'immagine, era in ascolto sulla porta 8080.
  • Quando si usano immagini scaricate dal Docker Hub è necessario:
    • avere la loro documentazione
    • fidarsi che non compiano operazioni malefiche
docker run -d -p 8080 adejonge/helloworld

Avviene il Port Publishing parziale della porta 8080 del container ad una porta casuale della macchina host

Verificare il container con:

docker ps

Dal rapporto scopriamo la porta dello host che mappa al container, indichiamola con PH:

0.0.0.0:PH -> 8080/tcp

L'indirizzo IP 0.0.0.0 copre qualsiasi indirizzo IP della macchina host, sia quelli esterni che localhost.

Dallo host, o da qualsiasi altra macchina della rete su cui è lo host, puntare il browser a:

http://IPH:PH/

Il messaggio viene visualizzato

Terminare e cancellare il contenitore precedente:

docker ps

docker rm -f ID

Lanciare un container con Port Publishing completo:

docker run -d -p 8000:8080 adejonge/helloworld

Publish della porta 8080 del container alla porta 8000 dello host

Verificare col browser a http://localhost:8000

Infatti a qualsiasi indirizzo su cui lo host è in ascolto

Reti con nome

Creazione Contenitori di Rete

Docker gestisce reti interne. Listarle:

docker network ls

Vi sono tre reti (driver) di base:

  • bridge – sottorete di default per tutti i container
  • host – rete dello host
  • none - solo interfaccia di loopback

Quando si usano ambienti più complessi del Docker Base, per esempio Docker Swarm, vengono installati anche alri tipi di driver di rete, p.es. overlay.

Ispezionare i dettagli di rete (formato JSON):

docker network inspect bridge

Creare una nuova rete bridge:

docker network create net1

Lanciare due containers sulla nuova rete:

docker run -ti --net=net1 --name one alpine
docker run -ti --net=net1 --name two alpine

Si può uscire da un contenitore senza terminarlo con i tasti Ctl-P Ctrl-Q.

Per ritorbare dentro il contenitore, per esempio:

docker attach one

Da one:

ping two

Docker risolve internamente i nomi dei container nel loro indirizzo IP.

Namednet

Opzioni di Creazione Rete

Sintassi:

docker network create [opzioni] rete

Opzioni principali:

  • --subnet CIDR - indirizzo di rete assegnato
  • --internal - solo accesso da locale, non internet
  • --driver nomedriver - default: bridge
  • --gateway gw - indirizzo del gateway
  • --attachable - si possono connettere containers alla rete
  • --ipv6 - abilita IPv6 sulla rete

Esempio:

docker network create --subnet 172.50.0.0/16 net2

Rimozione di Rete

Sintassi:

docker network rm rete

Esempio:

docker network rm net2

Si può rimuovere solo una rete per volta.

La rete che viene rimossa non deve avere contenitori che la usano. Prima rimuovere i contenitori.

Reti Host

Lo Host Networking consiste nel mappare tutte le porte del container alle stesse porte dello host.

Esempio con Host Networking:

docker run -d --net=host adejonge/helloworld
  • Condivide l'indirizzo IP dello host
  • Non viene più usato il bridge vethxxxx

Verificare con ifconfig e docker ps

Solo un container può usare la porta 8080 se in modalità Host Networking.

Dockerfile

Dfile

Un Dockerfile è un file di specifiche per la creazione di un'immagine Docker.

Il file deve chiamarsi Dockerfile e deve essere in una directory di contesto: vedrà tutti i files al di sotto di essa.

Non usare mai la directory radice come directory di contesto.

Tutti i files della directory di contesto e sottodirectories devono essere leggibili e scrivibili.

Il file è in formato ASCII (veramente UTF-8) e contiene una serie di direttive come parole chiave.

Le direttive non sono case-sensitive, ma vengono convenzionalmente poste in maiuscolo.

Important
L'arte di costruzione di un Dockerfile complesso è molto importante: permette di inserire un applicativo da noi sviluppato all'interno di una immagine Docker.
Tale immagine viene poi tipicamente usata in ambienti di esecuzione distribuita, come p.es. Kubernetes.

Build di immagini

Semplice Dockerfile

Occorre creare il Dockerfile.

Vogliamo un'immagine per il contatore infinito:

mkdir -p ~/docker/ex/dk1 
cd ~/docker/ex/dk1
vim forever.sh
#! /bin/bash
x=0 
while true
do
  x=$[x + 1] 
  echo $x 
  sleep 1 
done

Editare il Dockerfile:

vim Dockerfile
FROM ubuntu
MAINTAINER Hugh Fray <hugh@stormforce.ac>
COPY forever.sh /usr/local/bin/forever.sh
RUN chmod +x /usr/local/bin/forever.sh
CMD ["/usr/local/bin/forever.sh"]

Costruire l'immagine:

docker build -t forever1 .

Ove:

  • -t forever1 : nome che diamo all'immagine
  • . : directory di build

Verificare e lanciare l'immagine costruita:

docker images
docker run -d --name my_forever1 forever1
docker logs my_forever1

Dockerfile più complesso

Un possibile sempio di Dockerfile per nginx.

Nella directory di contesto:

mkdir -p ~/docker/ex/nginx-ubuntu
cd ~/docker/ex/nginx-ubuntu
vim Dockerfile
FROM ubuntu

# Install Nginx.
RUN \
  apt-get update && \
  apt-get install -y nginx && \
  rm -rf /var/lib/apt/lists/* && \
  echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \
  chown -R www-data:www-data /var/lib/nginx

# Define mountable directories.
VOLUME ["/etc/nginx/sites-enabled", "/etc/nginx/certs", "/etc/nginx/conf.d", "/var/log/nginx", "/var/www/html"]

# Define working directory.
WORKDIR /etc/nginx

# Define default command.
CMD ["nginx"]

# Expose ports.
EXPOSE 80
EXPOSE 443

Perchè tutti gli &&

Si può avere in alternativa:

  • una serie di comandi RUN ciascuno dei quali compie un'operazione
  • un unico comando RUN che compie più operazioni concatenate da &&

Il secondo metodo è migliore per i seguenti motivi:

  • Ogni RUN produce un'immagine intermedia, appesantendo il processo di generazione
  • L'operatore && non esegue il comando che segue se quello precedente è fallito e termina subito il processamento del Dockerfile.

Ogni singolo RUN di successo procede al prossimo RUN e se uno degli ultimi fallisce, occorre più tempo per interrompere il processamento del Dockerfile.

Generazione di Immagine

Posizionarsi nella directory di contesto

  • contiene il Dockerfile e tutto quello che serve

Generazione:

docker build [opzioni] -t immagine .

Opzioni principali:

  • --no-cache - ricostruisce completamente da zero
  • -q - sopprime i messaggi di progresso ad output

Vi sono numerose altre opzioni. Listarle con:

docker build --help

Nel nostro esempio:

cd ~/docker/ex/nginx-ubuntu
docker build -t nginx-ubuntu .

Con base Ubuntu è un processo piuttosto lungo.

Direttive del Dockerfile

Direttive Comuni

FROM

FROM immagine - specifica l'Immagine di Base da cui si sta costruendo. Deve essere la prima istruzione del file.

RUN

Esecuzione di un comando all'interno dell'immagine in costruzione.

Vi sono due tipi di RUN:

  • RUN comando - il comando è eseguito in una shell, che per default è /bin/sh -c su Linux
  • RUN ["eseguibile", "param1", "param2"] - vettore di lancio di un comando qualsiasi, generato con exec

Un comando RUN che si estende su più linee ha ciascuna linea terminata dal carattere di escape, di default \, immediatamente prima del NEWLINE.

LABEL

LABEL chiave=valore ...

Etichette a chiave.

MAINTAINER

MAINTAINER email - gestore del Dockerfile corrente

Ddeprecato, usare p.es.

LABEL maintainer="michele@simioli.it"

CMD

CMD comando - definisce il comando da eseguire al lancio del container, deve essercene esattamente uno.

Tre varianti di CMD:

  • CMD ["eseguibile","param1","param2"] - vettore di lancio in formato exec
  • CMD comando param1 param2 - formato shell
  • CMD ["param1", "param2"] - parametri relativi allo ENTRYPOINT

ENTRYPOINT

Sintassi come CMD ma solo nei due formati:

  • ENTRYPOINT ["eseguibile", "param1", "param2"]
  • ENTRYPOINT comando param1 param2

WORKDIR

WORKDIR dir - directory di lavoro per il comando, una sola

ADD

ADD sorgente... destinazione - aggiunge i file locali sorgente alla destinazione sull'immagine

  • sorgente può contenere caratteri jolly, e deve essere nel contesto del build
  • destinazione deve essere un percorso assoluto o relativo a WORKDIR
  • la destinazione appartiene a UID=0 e PID=0
  • se sorgente o destinazione terminano con / sono considerate directories
  • in caso di copia multipla la destinazione deve essere una directory
  • se la destinazione è una directory innestata, tutti i percorsi intermedi vengono creati

COPY

Stesso effetto e sintassi di ADD.

COPY sorgente... destinazione

ENV

ENV chiave[=]valore ... - settaggio di variabili d'ambiente mell'immagine

  • ci può essere un = o uno spazio come separatore
  • ve ne possono essere più di uno

VOLUME

VOLUME "dir" - crea un punto di montaggio per un volume fornito esternamente

  • deve esserci un solo argomento, ma può essere un array in formato JSON
  • "dir" deve essere un percorso assoluto

EXPOSE

EXPOSE porta - porta TCP/IP su cui ascoltare a runtime

USER

USER utente - definisce l'utente coi cui diritti viene eseguito CMD

Direttive Meno Comuni

SHELL

SHELL ["eseguibile", "parametro"] - definisce la shell con cui eseguire i comandi seguenti

  • default: SHELL ["/bin/bash", "-c"]
  • ve ne può essere più di uno definito nel Dockerfile: determina il comportamento per le istruzioni seguenti

ONBUILD

ONBUILD istruzione - definisce un trigger, un comando che deve venire eseguito quando l'immagine corrente è nel FROM di un altro Dockerfile

  • ve ne può essere più di uno

ARG

ARG nome[=valore] - definisce un argomento per l'opzione di build --build-arg nome=valore

  • ve ne può essere più di uno
  • tutti gli argomenti forniti nel comando build devono essere previsti nel Dockerfile
  • se un argomento non è fornito nel comando di build, ARG deve dargli un valore di default

STOPSIGNAL

STOPSIGNAL segnale - determina il segnale che viene inviato al container dal comando docker stop ID - default SIGTERM

Esempio d'uso

Esempio: nginx-alpine

Nginx

Illustra l’importanza della Directory di Progetto (o di Contesto).

In un’immagine possono essere inseriti dei file

  • File di configurazione
  • Procedure shell
  • ecc.

Tutti tali file si trovano nella directory di progetto, oppure in sue sottodirtctories.

La Directory di Progetto contiene il Dockerfile.

Directory di progetto:

mkdir -p ~/docker/ex/nginx-alpine
cd ~/docker/ex/nginx-alpine
vim Dockerfile
FROM alpine

# Install nginx package and remove cache
RUN apk add --update nginx && rm -rf /var/cache/apk/*

# Copy basic files
COPY nginx.non-root.conf /etc/nginx/nginx.conf
COPY index.html /usr/share/nginx/html/index.html

EXPOSE 8080
VOLUME ["/usr/share/nginx/html"]

# root user will run 'nginx: master process'
# nobody user will run 'nginx: worker process' as dictated in the nginx.non-root.conf
CMD ["nginx", "-g", "daemon off;"]

Nginx ha bisogno di un file di configurazione:

vi nginx.non-root.conf
worker_processes 1;
user nobody nobody;
error_log /dev/stdout;
pid /tmp/nginx.pid;
events {
  worker_connections 1024;
}
http {
    # Set an array of temp and cache files options 
    # that otherwise defaults to
    # restricted locations accessible only to root.
    client_body_temp_path /tmp/client_body;
    fastcgi_temp_path /tmp/fastcgi_temp;
    proxy_temp_path /tmp/proxy_temp;
    scgi_temp_path /tmp/scgi_temp;
    uwsgi_temp_path /tmp/uwsgi_temp;
    # mime types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 8080;
        root /usr/share/nginx/html;
        access_log /dev/stdout;
        error_log /dev/stdout;
    }
}

E infine una pagina iniziale:

vim index.html
<html>
<head>
    <title>Nginx with Alpine Linux</title>
</head>
<body>
    <h1>Nginx with Alpine Linux</h1>
</body>
</html>

Build dell'immagine che chiameremo nginx-alpine:

docker build -t nginx-alpine .

Impiega molto meno tempo che con Ubuntu e l’immagine risultante è molto più piccola.

Test:

docker run -d -p 8888:8080 --name ngalp nginx-alpine

Verifica col browser a URL localhost:8888

Entrypoint

Un ENTRYPOINT ha i compiti di:

  • verificare la sintassi e le opzioni del CMD
  • inizializzare l’ambiente

Un ENTRYPOINT è una procedura shell con la struttura:

#! /bin/sh

.....

exec “$@”

La procedura deve iniziare con lo hash-bang, nel nostro caso #! /bin/sh

L’ultima istruzione eseguita è il chain command: esegue il CMD con gli eventuali parametri.

Esempio: postgres-alpine

Postgres

Directory di progetto:

mkdir -p ~/docker/ex/postgres-alpine
cd ~/docker/ex/postgres-alpine

Dockerfile;

vim Dockerfile
FROM alpine:3.1

RUN apk add --update postgresql curl
RUN curl -o /usr/local/bin/gosu \
  -SL "https://github.com/tianon/gosu/releases/download/1.2/gosu-amd64"
RUN chmod +x /usr/local/bin/gosu
RUN apk del curl && rm -rf /var/cache/apk/*

RUN mkdir /docker-entrypoint-initdb.d
RUN mkdir -p /var/run/postgresql && \
    chown -R postgres /var/run/postgresql

ENV LANG en_US.utf8
ENV PATH /usr/lib/postgresql/9.3/bin:$PATH
ENV PGDATA /var/lib/postgresql/data

VOLUME /var/lib/postgresql/data

COPY docker-entrypoint.sh /
RUN chmod +x docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

USER postgres
EXPOSE 5432
CMD ["postgres"]

Notare che la prima direttiva, FROM alpine:3.1, ha un'immagine con tag.

Non vogliamo usare il tag implicito latest ma una versione di alpine ben nota.

Con opportuna ricerca documentale in rete, abbiamo infatti scopero che alpine 3.1 ha nei suoi repository software la release postgres 9.3, che è la specifica versione che vogliamo installare.

Nostro Entrypoint

Uno entrypoint ha il compito di preparare l’ambiente al CMD che poi viene eseguito.

E’ quasi sempre una procedura shell.

Requisiti per la preparazione di immagini:

  • conoscenza di comandi amministrativi Linux nella distribuzione scelta per l’immagine di base
    • Ubuntu vs Alpine
  • conoscenza della programmazione shell
  • conoscenza di utilities che permettano il cambiamento batch e non interattivo di files di configurazione
    • sed, awk, tr, ecc.
vim entrypoint.sh
#!/bin/sh
chown -R postgres "$PGDATA"
# if the datadir does not exist, create it
if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
# listen on all network addresses
        sed -ri "s/^#(listen_addresses\s*=\s*)\S+/\1'*'/" "$PGDATA"/postgresql.conf
# acquire or set environment variables
        : ${POSTGRES_USER:=postgres}
        : ${POSTGRES_DB:=$POSTGRES_USER}
# if the password exists, use it with MD5
        if [ "$POSTGRES_PASSWORD" ]; then
                pass="PASSWORD '$POSTGRES_PASSWORD'"
                authMethod=md5
# otherwise warn, then trust an access without it
        else
                cat >&2 <<-'EOWARN'
****************************************************
WARNING: No password has been set for the database. 
****************************************************
EOWARN
                pass=
                authMethod=trust
        fi
# prepare the rest of the environment
        if [ "$POSTGRES_DB" != 'postgres' ]; then
                gosu postgres postgres --single -jE <<-EOSQL
                  CREATE DATABASE "$POSTGRES_DB" ;
EOSQL
                echo
        fi

        if [ "$POSTGRES_USER" = 'postgres' ]; then
                op='ALTER'
        else
                op='CREATE'
        fi

        gosu postgres postgres --single -jE <<-EOSQL
                $op USER "$POSTGRES_USER" WITH SUPERUSER $pass ;
EOSQL
        echo
# allow anybody to connect from any IP address
        { echo; echo "host all all 0.0.0.0/0 $authMethod"; } >> "$PGDATA"/pg_hba.conf
        exec gosu postgres "$@"
fi
exec "$@"

Important Le procedure shell, specie per entrypoint, possono essere complesse. Occorre acquisire un certo livello di esperienza di programmazione shell.

Utility gosu

L’invocazione di una procedura shell la esegue in una sottoshell, figlia di quella corrente.

  • Questo non si deve fare in un Entrypoint, se si vogliono compiere cambiamenti all’ambiente che durino
  • Al termine della sottoshell i cambiamenti sparirebbero

Si può usare gosu:

  • gosu esegue il suo argomento nella shell corrente, non in una sottoshell
  • riceve eventuali segnali inviati alla shell originale
  • è un programma originalmente scritto in Go

Alpine lo ha come package nativo nelle versioni più recenti, in quelle precedenti occorre scaricarlo da https://github.com/tianon/gosu

Build e Test

Build:

docker build -t postgres-alpine .

Lancio del contenitore:

docker run --name postalp -p 5435:5432 postgres-alpine

(porta 5435 non 5432 sullo host per evitare potenziali collisioni con un postgres già installato)

Tattica di debugging:

  • La prima volta non mettere l’opzione -d (detached)
    • L’output andrà su video
    • Permette il debugging specie di docker-entrypoint.sh
  • Fermare con Ctrl-C e rimuovere il contenitore
  • Le volte successive mettere l’opzione -d

Strategie d'Uso

Commit o Dockerfile

Due modi per creare un'immagine Docker:

  1. creare un Dockerfile e lanciare un docker build
  2. run di un container, modificarlo, e lanciare il comando docker commit contenitore immagine per generare una nuova immagine

Meglio il Dockerfile:

  • L'immagine di base può non essere adatta ai nostri scopi e vogliamo successivamente cambiarla
    • col commit è praticamente impossibile
    • col Dockerfile basta cambiare una linea
  • Col commit non si può riprodurre un'immagine
  • Il Dockerfile produce documentazione dei passi che sono stati seguiti nella costruzione dell'immagine
  • Nel Dockerfile si possono inserire campi LABEL poi visibili col comando docker inspect
    • E' possibile copiare il Dockerfile stesso nell'immagine che viene costruita, come ulteriore documentazione portatile

Dockerfiles in Rete

In un mondo realistico moderno:

  • il sofware è disponibile in formato Open Source
  • per molti applicativi i Dockerfile sono reperibili in rete
  • le immagini Docker già pronte possono essere problemi di sicurezza

Esempio: nginx

Troviamo il sito del Dockerfile e scarichiamolo

git clone https://github.com/nginxinc/docker-nginx.git

Andiamo nella directory di contesto generata dal clone:

cd docker-nginx/stable/alpine

Generiamo il progetto:

docker build -t nginx-alpine-src .

Occorrerà molto tempo per la compilazione dei sorgenti, ma non dovrebbero esservi errori.

Miracolo Open Source non Docker.

Complementi

Il mondo Docker è vasto. Vi sono parecchi aspetti aggiuntivi che si potrebbero trattare rispetto ad un'esposizione di base.

Gli argomenti di questa sezione non sono indispensabili, ma aggiungono un certo livello di comprensione.

Vengono considerati esempi a supporto delle seguenti asserzioni forti:

  1. Non è una buona idea creare immagini contenenti applicativi grafici interattivi
  2. Il miglior linguaggio di programmazione per lo sviluppo di applicativi da inserire in immagini Docker, è il Linguaggio Go

Clients X Window

Supponiamo di voler costruire un'immagine che produca il client grafico Firefox.

mkdir -p ~/docker/ex/xwin
cd ~/docker/ex/xwin
vim Dockerfile
FROM ubuntu:14.04

RUN apt-get update && apt-get install -y firefox

# Replace 1000 with your user / group id
RUN export uid=1000 gid=1000 && \
mkdir -p /home/developer && \
echo "developer:x:${uid}:${gid}:Developer,,,:/home/developer:/bin/bash" >> /etc/passwd && \
echo "developer:x:${uid}:" >> /etc/group && \
echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer && \
chmod 0440 /etc/sudoers.d/developer && \
chown ${uid}:${gid} -R /home/developer

USER developer
ENV HOME /home/developer
CMD /usr/bin/firefox

Costruire l’immagine con:

docker build -t firefox .

Impiega un tempo lunghissimo.

Deve scaricare e installare circa cento componenti e librerie che compongono il sistema X Window.

Docker è inteso per i server, non per i client grafici.

Al termine, lanciare con:

docker run -ti --rm \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  firefox

C’è un mappaggio del socket unix di X Window

  • X Window in Ubuntu funziona solo con socket Unix non Inet

Se di usa --net=host per mappare tutti i socket:

docker run --rm -e DISPLAY --net=host firefox

Immagini e linguaggio Go

Immagine con Compilatore Go

Usiamo il metodo manuale:

  • partire da un'immagine di base
  • aggiungere il software necessario
  • clonare il contenitore in una nuova immagine

Partiamo da Alpine:

docker run -ti --name alpine alpine sh

Aggiorniamo l’ambiente e aggiungiamo pacchetti:

apk update
apk add vim
apk add git

Installiamo il supporto go per vim da Github:

cd
git clone https://github.com/fatih/vim-go.git \
~/.vim/pack/plugins/start/vim-go

Installiamo go:

apk add go
apk add musl-dev

Alpine usa la libreria runtime MUSL non GLIBC. Occorre installarne il pacchetto di sviluppo.

Prepariamo lo scaffolding di go:

su -
mkdir -p ~/go/src ~/go/bin ~/go/pkg

Aggiunta dei plugin go a vim. Lanciare vim:

vim

Dare il comando vim (preso dal supporto installato)

:GoInstallBinaries

La possibilità di dare comandi interattivi uno dei motivi per cui alle volte si preferisce una costruzione manuale di immagine a quella tramite Dockerfile. Quest'ultimo permette solo l'esecuzione di comandi batch.

Attendere il completamento e uscire da vim con :q Enter

Prepariamo l’ambiente editando /root/.profile:

cat <<EOF >> /root/.profile
export GOROOT=/usr/lib/go
export GOPATH=/root/go
export GOBIN=\$GOPATH/bin
export GO111MODULE=off
export PATH=~/bin:\$GOBIN:\$GOROOT/bin:\$PATH
EOF

Uscire dal contenitore con Ctrl-P Ctrl-Q, NON col comando exit.

Sulla macchina host generiamo la nuova immagine:

docker commit alpine vimgo

Attenzione: è grosso, ci vuole tempo.

Uso di vimgo

Creare una directory locale da condividere:

mkdir -p ~/bin

Lanciare il contenitore:

docker run --rm -ti --name vimgo \
  -v ~/bin:/root/go/bin vimgo su -

Il comando eseguito, su -, lancia una shell di login di root, che dal .profile setta tutte le variabili d'ambiente.

E' necessario eseguire su - perchè se si esegue semplicemente sh parte una shell senza ambiente.

Scrittura di un semplice programma in Go:

cd go/src
mkdir hello
cd hello
vim hello.go
package main

import "fmt"

func main() {
  fmt.Println("vim-go")
}

Compilazione statica:

go build --buildmode=exe --ldflags '-linkmode external \
  -extldflags "-static" -s' hello.go

Copia nella cartella condivisa:

cp hello ~/go/bin

Uscire dal contenitore con exit. Il contenitore è automaticamente rimosso poichè era stato lanciato con l'opzione --rm.

Provare in locale:

~/bin/hello

Immagine e Go

Produrremo un altro eseguibile Go linkato staticamente e lo inseriremo in un'immagine.

Preparazione del progetto:

mkdir -p ~/docker/ex/go
cd ~/docker/ex/go

Partenza di Vimgo:

```bash
docker run --rm -ti --name vimgo \
  -v $PWD:/root/go/bin vimgo su -

Questa volta mappiamo la directory corrente a quella degli eseguibili Go sul contenitore.

Prepariamo lo scaffolding:

cd go/src
mkdir helloworld
cd helloworld
vim helloworld.go
package main

import (
  "fmt"
  "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello World from Go in Docker")
}

func main() {
  http.HandleFunc("/", helloHandler)

  fmt.Println("Started, serving at 8080")
  err := http.ListenAndServe(":8080", nil)
  if err != nil {
    panic("ListenAndServe: " + err.Error())
  }
}

E’ un web server sulla porta 8080 che da un semplice messaggio.

Compilazione statica:

go build --buildmode=exe --ldflags '-linkmode external \
  -extldflags "-static" -s' helloworld.go

Copia nella cartella condivisa:

cp helloworld ~/go/bin

Uscire dal contenitore con exit. Il contenitore è automaticamente rimosso.

Generazione dell'Immagine

Dovrebbe esserci nella directory corrente il file eseguibile helloworld, prodotto col contenitore.

Scrivere il Dockerfile:

vim Dockerfile
FROM scratch
ADD ./helloworld /helloworld
EXPOSE 8080
CMD ["/helloworld"]

scratch è il nome di un'immagine vuota.

Un programma Go è già il 99% di un’immagine.

Solitamente si parte da un'immagine di base perche fornisce uno scheletro di ambiente operativo di cui il nostro applicativo ha bisogno: librerie, ma anche aggancio a funzionalità del kernel, come gestione memoria e schedulazione.

Un eseguibile Go linkato staticamente non ha bisogno di librerie. Inoltre è fornito di serie di un monitor operativo, nello spazio user, che fornisce:

  • schedulazione concorrente
  • allocazione di memoria
  • garbage collection

Non ha quindi bisogno neppure di uno stub di sistema operativo sottostante.

Compiere il build:

docker build -t greet .

Lanciare un container per testare:

docker run -d --rm --name saluti greet

Controllare alla URL: localhost:8080.

Al termine fermare il container che verrà automaticamente rimosso:

docker stop saluti

Chain Build

Possiamo compiere due migliorie:

  • usare un'immagine per il compilatore Go già disponibile su Docker Hub, anzichè la nostra vimgo
  • compiere la compilazione e subito la preparazione di un'immagine finale con un unico Dockerfile, usando la tecnica del chain build

Preparare un programma demo:

mkdir -p ~/docker/ex/demo
cd ~/docker/ex/demo
vim main.go
package main

import (
  "fmt"
  "log"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello, 世界")
}
func main() {
  http.HandleFunc("/", handler)
  fmt.Println("Running demo app. Press Ctrl+C to exit...")
  log.Fatal(http.ListenAndServe(":8888", nil))
}

Preparare il Dockerfile.

vim Dockerfile
FROM golang:1.17-alpine AS build

WORKDIR /src/
COPY main.go go.* /src/
ENV GO111MODULE="off"
RUN CGO_ENABLED=0 go build -o /bin/demo

FROM scratch
COPY --from=build /bin/demo /bin/demo
EXPOSE 8888
ENTRYPOINT ["/bin/demo"]

Build dell’immagine myhello:

docker build -t myhello .

Test dell'immagine:

docker run -d --rm --name try -p 8888:8888 myhello

Connessione al contenitore:

curl localhost:8888

Al termine del test, stop del contenitore, che lo rimuove.

Sicurezza

Security

La sicurezza di un qualsiasi applicativo è un aspetto primario.

Anche applicativi in contenitori Docker hanno problemi di sicurezza. Questi possono essere dovuti a:

  • vulnerabilità di Docker - usare una versione recente del Docker Engine, ove siano state applicate le patch ai problemi di sicurezza riscontrati
  • esposizioni di configurazione - usare metodi di configurazione ed uso dell'ambiente Docker che minimizziono le esposizioni
  • mancata mitigazione di problemi - limitare le risorse disponibili ai contenitori Docker in modo da limitare l'impatto di minacce realizzate con successo

Contenitori e sicurezza

Considerazioni Generali

Vi possono essere notevoli problemi di sicurezza nell'uso di default di contenitori:

  • I permessi di default di docker sono quelli di root
  • Usare Docker vuol dire caricare immagini non garantite dalla rete e lanciarle coi permessi di root

“Will you walk into my parlour?” - said the spider to the fly.

Prima Regola: Non scaricare immagini non fidate

  • l'immagine ufficiale di un prodotto su Docker Hub si può considerare fidata
  • altre immagini di altri produttori, anche su Docker Hub, non sono da considerarsi fidate

Docker Hub da la possibilità di compiere l'upload di immagini pubbliche. Non ne compie un'analisi di sicurezza.

Le uniche immagini veramente fidate sono quelle che abbiamo costruito noi, manualmente o tramite Dockerfile.

Seconda Regola: Progettare i programmi dentro i container con le stesse avvedutezze dei programmi dello host

  • I server devono diminuire i privilegi
  • Meglio sottoporre i contenitori a pentesting
  • Controllare i log dello host

Terza Regola: Attenzione allo host mapping

Esempio pericoloso. Considerare il comando:

docker run --rm -v /:/homeroot -it alpine sh

Il processo sh del contenitore ha i diritti di root nel contenitore, ma anche sul sistema host se riesce ad uscirne. Il mappaggio -v /:/homeroot fa sì che modifiche alla cartella /homeroot del contenitore corrispondano a modifiche di / del sistema host.

Sicurezza del Server

Un server con docker dovrebbe avere solo docker come servizio attivo.

  • Spostare gli altri servizi ad altri server
  • Se non è possibile, spostare tutti gli altri servizi in containers
  • Si può lasciare sshd ma solo con autenticazione a chiave condivisa, non password

Il server docker espone una REST API al client docker su una socket unix.

E' possibile attivarlo su una socket inet ma è estremamente pericoloso perchè espone il servizio ad attacchi Cross Site Request Forgery (XSRF).

  • Limitare l'accesso al client docker - gli utenti che appartengono al gruppo docker sono fidati e responsabili, possibilmente già con mansioni amministrative sullo host
  • Lanciare i comandi docker dallo host locale - prima usare ssh per accedere al server docker, poi dare comandi che eseguono in locale

Contenitori in Macchine Virtuali

Questo aggiunge uno strato di sicurezza, ma va in controtendenza alla filosofia dei contenitori.

Un contenitore che conduca un attacco di diniego di servizio può riuscire al massimo a consumare tutte le risorse della VM, non del sistema host, di altri contenitori sul sistema host o di altri contenitori in altre VM.

E' una soluzione da adottarsi solo in caso di sospetto su un contenitore particolare, non di prammatica.

Idealmente:

  • le immagini di base sono derivate da repositories ufficiali di fiducia
  • le immagini dipendenti, che richiedono quelle base, hanno un Dockerfile di specifica
  • tutte le immagini derivate sono conservate su un repository locale

Docker Content Trust

Servizio offerto da Docker.io. Pull solo di immagini firmate dal fornitore.

Va abilitato:

export DOCKER_CONTENT_TRUST=1

Download del certificato di firma dal repository. Questo va accettato separatamente, come con SSH.

Ha un costo di licenza.

Permette anche l'upload di immagini firmate. Generata coppia di chiavi ed un certificato. Chiave privata in: ~/.docker/trust/private

Per scaricare un'immagine non firmata, quando il trust è abilitato, usare il comando:

docker pull --disable-content-trust immagine

Il Docker Content Trust non protegge da attacchi Man-In-The-Middle (MITM) o di Ingegneria Sociale.

Per configurare un repository locale con Docker Content Trust occorre installare un Notary Server. Anche questo è offerto come servizio aggiuntivo a licenza dalla Docker.io.

Capabilities

Da Linux 2.2 root non può fare tutto.

Esistono Capabilities (circa 30). Per listarle:

man capabilities

Root su host ha la capability sys-admin,

I container di docker non hanno tutte la capabilities dello host, ma un sottoinsieme limitato.

Si possono aggiungere e togliere capabilities.

Esempi:

docker run --cap-drop setuid --cap-drop setgid -ti ubuntu bash
docker run --cap-add all --cap-drop sys-admin  -ti ubuntu bash

Per lanciare un contenitore con tutte le capabilities aggiungere l'opzione --privileged.

Minimo Privilegio

Ogni container deve possedere i privilegi minimi per il suo lavoro e non di più.

Un container compromesso non deve essere in grado di compiere danni eccessivi.

La difesa consiste in:

  • assicurarsi che i processi nei contenitori non siano attivi come root
  • mappare i filesystems host come read-only se non vi è necessità di scrittura
  • ridurre le capabilities che un contenitore possiede
  • limitare le risorse host che un contenitore può usare

Limiti ai contenitori

Limitare il Networking

Aprire soltanto le porte che servono.

Di default più containers sulla stessa rete possono vedersi anche se le porte non sono state ufficialmente aperte od esposte.

Esempio. Scaricare l’immagine:

docker pull amouat/network-utils

Lanciare due containers:

docker run --name nc-test -d \
  amouat/network-utils nc -l 5001
docker run \
  -e IP=$(docker inspect -f \
  {{.NetworkSettings.IPAddress}} nc-test) \
  amouat/network-utils \
  sh -c 'echo  "hello" | nc -v $IP 5001'

Uscire dai container con Ctrl-P Ctrl-Q.

Ispezionare il risultato:

docker logs nc-test

Al termine, ripulire i container.

I contenitori si vedono tra loro anche se la porta di rete non è esplicitamente aperta.

Si può disabilitare la comunicazione tra i container lanciandoli con l'opzione --icc=false. Inoltre l'opzione --iptables=true forza l'uso delle regole di iptables sullo host.

Meglio ancora, lanciamo i contenitori su una rete docker interna. Questo limita la visibilità ai soli contenitori sulla stessa rete interna.

SUID e SGID

E' molto probabile che non vi sia bisogno nel nostro applicativo dei programmi con il bit SUID o SGID settato.

In tal caso può essere una buona idea disabilitarli.

Per esempio, in un Dockerfile:

FROM alpine
...
RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true
...

Limitare la Memoria

Scaricare il programma di stress test:

docker pull amouat/stress

Limitare memoria, swap e memoria virtuale°

docker run -m 128m --memory-swap 128m amouat/stress \
  stress --vm 1 --vm-bytes 127m -t 5s

Memoria virtuale eccessiva:

docker run -m 128m --memory-swap 128m amouat/stress \
  stress --vm 1 --vm-bytes 130m -t 5s

Non ponendo limiti allo swap:

docker run -m 128m amouat/stress \
  stress --vm 1 --vm-bytes 255m -t 5s

Non tutte le installazioni di Docker consentono limitazioni alla memoria virtuale. Dare il comando:

docker info

e cercare la presenza eventuale del messaggio

WARNING: No swap limit support

Il supporto richiederebbe una ricompilazione del kernel Linux.

Limitare la CPU

Con l'opzione -c.

Viene dato un peso relativo ad ogni contenitore per quanto riguarda l'impegno di CPU. Il peso è una potenza di 2. Il peso di default è 1024.

Esempio:

docker run -d --name load1 -c 2048 amouat/stress
docker run -d --name load2 amouat/stress
docker run -d --name load3 -c 512 amouat/stress
docker run -d --name load4 -c 512 amouat/stress

docker stats $(docker inspect -f {{.Name}} $(docker ps -q))

Interrompere docker stats con Ctrl-C. Ripulire i contenitori al termine dell'esercizio.

Limitare i Restart

Un container può avere problemi e compiere troppi restart. Limitarli con, per esempio:

docker run -d --restart=on-failure:10 my-flaky-image

Per vedere il conto di restart corrente:

docker inspect -f "{{ .RestartCount }}" $(docker ps -lq)

Limitare i Filesystem

Con l'opzione --read-only.

Esempio:

docker run --read-only debian touch x
touch: cannot touch 'x': Read-only file system

Si può inoltre limitare l'accesso ai volumi. Per esempio:

docker run -v $(pwd):/pwd:ro debian touch /pwd/x
touch: cannot touch '/pwd/x': Read-only file system

Docker Compose

Dcompose

Docker Compose è uno strumento per definire ed eseguire applicatvi multi-container.

Semplifica il controllo dell'intero stack applicatvo, rendendo facile la gestione di servizi, reti e volumi con un singolo file di configurazione Yaml.

Orchestrazione locale

Docker Compose è uno strumento per definire ed eseguire applicativi composti da più contenitori.

Sono chiamati servizi.

Basato su:

  • Un file di specifiche
  • Un singolo comando esecutivo

Azioni:

  • Start, stop e rebuild di servizi
  • Vedere lo stato e i log dei servizi attivi

Utile per:

  • Continuous Integration
  • Creare e distruggere ambienti di testing integrati

Docker Compose è uno strumento di orchestrazione sulla singola macchina host.

Compose1

Un applicativo può essere complesso e avere più containers intercollegati:

  • Sequenza di creazione?
  • Interconnessioni?
  • Dipendenze?

Compose2

Specifiche di applicativo complesso.

  • File di configurazione: docker-compose.yml
    • Linguaggio yaml
  • Lancio: docker-compose up

Installazione di Docker Compose

Installazione

Sito: https://docs.docker.com/compose/install/

Da terminale:

sudo curl -L "https://github.com/docker/compose/releases/download/1.28.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Test:

docker-compose version

Aiuto:

docker-compose --help

File di specifiche Yaml

Compose File

File di configurazione per la definizione dei servizi:

  • Containers da caricare
  • Volumi mappati
  • Reti di collegamento
  • Variabili d'ambiente
  • Porte esposte
  • ecc.

Nome di default: docker-compose.yml

Formato Yaml

  • Yet Another Markup Language – YAML Ain't Markup Language
  • Standard di serializzazione per qualsiasi linguaggio
  • Estensione .yml o .yaml
  • Regole strette per l’indentazione (come in Python)

Prova

Test con un semplice HelloWorld:

mkdir hello
cd hello
vi docker-compose.yml
version: '2'
services:
  hello:
    image: tutum/hello-world
    ports:
      - 80

Attenzioni:

  • Allineamenti e indentazioni
  • Devono essere spazi non Tab

Lancio:

docker-compose up -d
  • -d: detached – in background

Viene scaricata l'immagine tutum/helloworld.

  • Verificare con docker images.

Viene lanciato un contenitore;

  • Il contenitore è detached
  • Verificare la sua porta esterna con docker ps
  • Collegare un browser a questa porta

Ripulire.

Fermare e rimuovere il contenitore:

docker-compose down

Esercizio 1: una rete

01net1-ccli-cssh

Descrizione concisa: Una rete, client e server SSH.

E' importante preparare un diagramma del progetto.

Non esiste uno specifico linguaggio di diagrammazione, ciascuno può scegliersi il suo. L'importante è rimanere consistemti in diagrammi di più progetti.

Il diagramma deve specificare tutti i componenti del progetto. Lo scopo è di passarlo ad un programmatore, che deve comprendere subito quello che deve preparare.

01net

La rete si chiamerà net1 e il suo indirizzo CIDR sarà 192.160.100.0/24.

Sulla rete vi saranno due container: one e two, con indirizzi di host 11 e 12.

Il container one sarà generato dall'immagine ccli e sarà un client SSH, two dall'immagine cssh, un server SSH.

Il container two avrà un utente pippo, con password pluto.

Scaffolding

E' importante avere un nome distintivo del progetto, che suggerisca la struttura e le intenzioni di ciò che si vuole compiere.

Creare la directory di progetto:

mkdir 01net1-ccli-cssh
cd 01net1-ccli-cssh

L'albero dei files che comporranno il progetto è preparato prima, con i files vuoti. Questo è lo scaffolding (impalcatura) del progetto.

L'approccio è top-down.

Preparare lo scaffolding:

mkdir ccli cssh
touch ccli/Dockerfile cssh/Dockerfile
touch docker-compose.yml

Ogni immagine da generare ha una sottodirectory col nome dell'immagine, e contiene il suo Dockerfile, più ogni altro eventuale file necessario nal build.

A livello progetto vi è il file docker-compose.yml.

Il risultato è:

tree .
.
├── ccli
│   └── Dockerfile
├── cssh
│   └── Dockerfile
└── docker-compose.yml

Files del Progetto

ccli/Dockerfile

vim ccli/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

RUN apk --update add --no-cache openssh tcpdump curl

CMD ["/bin/sleep","1000000"]

cssh/Dockerfile

vim cssh/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

# Installazione del software
RUN apk --update add --no-cache openssh tcpdump \
openssh bash && rm -rf /var/cache/apk/*

# ‘root’ può fare login
RUN sed -i s/#PermitRootLogin.*/PermitRootLogin\ yes/ /etc/ssh/sshd_config \
  && echo "root:root" | chpasswd 
# Abilitare la porta 22
RUN sed -ie 's/#Port 22/Port 22/g' /etc/ssh/sshd_config

# Abilitare le chiavi crittografiche
RUN sed -ri 's/#HostKey \/etc\/ssh\/ssh_host_key/HostKey \/etc\/ssh\/ssh_host_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_rsa_key/HostKey \/etc\/ssh\/ssh_host_rsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_dsa_key/HostKey \/etc\/ssh\/ssh_host_dsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ecdsa_key/HostKey \/etc\/ssh\/ssh_host_ecdsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ed25519_key/HostKey \/etc\/ssh\/ssh_host_ed25519_key/g' /etc/ssh/sshd_config

# Generare le chiavi crittografiche
RUN /usr/bin/ssh-keygen -A
RUN ssh-keygen -t rsa -b 4096 -f  /etc/ssh/ssh_host_key

# Generazione dell’utente ‘pippo’ con password ‘pluto’
RUN adduser -D pippo && echo "pippo:pluto" | chpasswd

# Porta da esporre al port mapping
EXPOSE 22

# Comando da lanciare
CMD ["/usr/sbin/sshd","-D"]

docker-compose.yml

vim docker-compose.yml
version: '3.6'

services:
  one:
    build: ccli
    image: ccli
    container_name: one
    hostname: one
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.11
  two:
    build: cssh
    image: cssh
    container_name: two
    hostname: two
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.12
networks:
  net1:
    name: net1
    ipam:
      driver: default
      config:
        - subnet: 192.168.101.0/24

Esecuzione

Lancio del progetto:

docker-compose up -d

Vengono create le immagini ccli e cssh.

Vengono attivate la rete net1 e i containers one e two.

Test

Collegarsi a one:

docker exec -ti one sh

Aprire una sessione SSH a two:

ssh pippo@two
  • Accettare la chiave pubblica
  • La password è pluto

Chiudere il collegamento ssh con exit.

Uscire da one con exit.

Pulizia

Da host, directory di progetto:

docker-compose down

Vengono fermati i contenitori, poi rimossi i contenitori e la rete.

Esercizio 1A

Una rete, client e server SSH.

Collegamento ai container da Gnome Terminal.

01anet

Profilo di terminale

Nuovo profilo: one.

Term1

Comando di lancio:

bash -c "docker exec -ti one sh -c \"export PS1='\h:\W\# ' && sh"\"

Term2

Per renderlo distintivo, cambiamo i colori del terminale.

Term3

Nuova finestra di terminale e collegamento con SSH.

Term4

Profilo per two

Ripetere per un nuovo profilo two.

Schema di colori: Green on black.

Term5

Esercizio 2: comunicazione con host

02net1-ccli-chttp

Una rete, client e server HTTP.

Collegamento a server HTTP dal browser dello host.

02net

Scaffolding

Creare la directory di progetto:

mkdir 02net1-ccli-chttp
cd 02net1-ccli-chttp

Preparare lo scaffolding. Il risultato è:

.
├── ccli
│   └── Dockerfile
├── chttp
└── docker-compose.yml

Il file ccli/Dockerfile è lo stesso dell’esercizio precedente°

cp ../01net1-ccli-cssh/ccli/Dockerfile ccli

Il server HTTP userà un’immagine dal Docker Hub, non vi sarà bisogno di costruirne una.

docker-compose.yml

vim docker-compose.yml
version: '3.6'

services:
  one:
    build: ccli
    image: ccli
    container_name: one
    hostname: one
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.11
  two:
    image: httpd:2.4-alpine
    container_name: two
    hostname: two
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.12
networks:
  net1:
    name: net1
    ipam:
      driver: default
      config:
        - subnet: 192.168.101.0/24

Lancio

Term10

Accesso da Browser esterno

Occorre compiere un port mapping.

Modificare docker-compose.yml per supportarlo:

...
  two:
    image: httpd:2.4-alpine
    container_name: two
    hostname: two
    cap_add:
      - ALL
    ports:
      - 8888:80
    networks:
      net1:
        ipv4_address: 192.168.101.12

...

Esecuzione

Partenza del progetto:

docker-compose up -d

Aprire un browser sulla macchina virtuale.

Collegarsi a http://localhost:8888 e verificare.

Term11

Esercizio 2A

Una rete, client e server HTTP.

Da tcpdump di un container a Wireshark sullo host.

Term12

Cooperazione tra Host e Container

Installare Wireshark sulla macchina host:

sudo apt install wireshark-qt

Richiede tempo.

Far partire il progetto:

docker-compose up -d

Dalla macchina host:

Lanciare Wireshark che si collega al tcpdump del container one

sudo wireshark -i <(docker exec one tcpdump -i eth0)

Qualsiasi interfaccia vethxxx va bene.

Su one:

Aprire un terminale.

Collegarsi al server HTTP con curl:

curl -v http://two

Monitorare il traffico su Wireshark.

Esercizio 3: due reti e routing

03net1-ccli-net2-cssh

Due reti, client e server SSH su reti diverse.

Problema del routing.

03net

Scaffolding

Creare la directory di progetto:

mkdir 03net1-ccli-net2-cssh
cd 03net1-ccli-net2-cssh

Lo scaffolding è:

.
├── ccli
│   └── Dockerfile
├── cfw
│   └── Dockerfile
├── cssh
│   └── Dockerfile
└── docker-compose.yml

I Dockerfile di ccli e cssh sono gli stessi dell’esercizio 01net1-ccli-cssh.

Creare uno Gnome-terminal per three.

Files

cfw/Dockerfile

vim cfw/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

RUN apk --update add --no-cache openssh tcpdump curl iptables

CMD ["/bin/sleep","1000000"]

docker-compose.yml

vim docker-compose.yml
version: '3.6'

services:
  one:
    build: ccli
    image: ccli
    container_name: one
    hostname: one
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.11
  two:
    build: cssh
    image: cssh
    container_name: two
    hostname: two
    cap_add:
      - ALL
    networks:
      net2:
        ipv4_address: 192.168.102.12
  three:
    build: cfw
    image: cfw
    container_name: three
    hostname: three
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.10
      net2:
        ipv4_address: 192.168.102.10
networks:
  net1:
    name: net1
    ipam:
      driver: default
      config:
        - subnet: 192.168.101.0/24
  net2:
    name: net2
    ipam:
      driver: default
      config:
        - subnet: 192.168.102.0/24

Raggiungibilità

Aprire i terminali one, two, three.

Su one (192.168.101.11):

ping 192.168.101.10 - funziona

ping 192.168.102.12 - non funziona

Su two (192.168.102.12):

ping 192.168.102.10 - funziona

ping 192.168.101.11 - non funziona

Manca il routing. Aggiungerlo manualmente.

Su one:

ip route add 192.168.102.0/24 via 192.168.101.10

Su two:

ip route add 192.168.101.0/24 via 192.168.102.10

Riprovare la raggiungibilità. Ora funziona.

Ma vogliamo che vi sia in automatico al lancio dei containers, non aggiungerla a mano.

Useremo degli entrypoints.

Modifica al Client

Modifica a one (ccli).

vi ccli/entrypoint.sh
#! /bin/sh
  
echo "Waiting 2 seconds for router"
sleep 2
ip route add 192.168.102.0/24 via 192.168.101.10 || true

exec "$@"
vi ccli/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

RUN apk --update add --no-cache openssh tcpdump curl

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/bin/sleep","1000000"]

Modifica al Server

Modifica a two (cssh).

vi cssh/entrypoint.sh
#! /bin/sh
  
echo "Waiting 2 seconds for router"
sleep 2
ip route add 192.168.101.0/24 via 192.168.102.10 || true

exec "$@"
vi cssh/Dockerfile
....
RUN ssh-keygen -t rsa -b 4096 -f  /etc/ssh/ssh_host_key
RUN adduser -D pippo && echo "pippo:pluto" | chpasswd

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
EXPOSE 22
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/sbin/sshd","-D"]

Modifica a docker-compose.yml

vim docker-compose.yml
version: '3.6'

services:
  one:
    build: ccli
    image: ccli
    container_name: one
    hostname: one
    depends_on:
      - three
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.11
  two:
    build: cssh
    image: cssh
    container_name: two
    hostname: two
    depends_on:
      - three
    cap_add:
      - ALL
    networks:
      net2:
        ipv4_address: 192.168.102.12
  three:
    build: cfw
    image: cfw
    container_name: three
    hostname: three
    cap_add:
      - ALL
    networks:
      net1:
        ipv4_address: 192.168.101.10
      net2:
        ipv4_address: 192.168.102.10
networks:
  net1:
    name: net1
    ipam:
      driver: default
      config:
        - subnet: 192.168.101.0/24
  net2:
    name: net2
    ipam:
      driver: default
      config:
        - subnet: 192.168.102.0/24

Riprova

Cancellare le immagini ccli e cssh:

docker rmi ccli:latest cssh:latest

Lanciare il progetto:

docker-compose up

Non dare -d la prima volta, per vedere i logs.

Vengono ricreate le immagini ccli e csh.

Aprire i terminali one e two che si pingano a vicenda. Dovrebbero vedersi.

Esercizio 4: due reti e firewall

04net1-ccli-net2-cssh-chhtp

Due reti, server HTTP su seconda rete.

Solo server SSH visibile da prima rete.

04net

Scaffolding

Directory di progetto:

mkdir 04net1-ccli-net2-cssh-chhtp
cd 04net1-ccli-net2-cssh-chhtp

Copiare il progetto precedente:

cp -Rv ../03net1-ccli-net2-cssh/* .

Creare gli elementi nuovi:

mkdir chttp
touch chttp/entrypoint.sh
touch chttp/Dockerfile

L’albero risultante è:

.
├── ccli
│   ├── Dockerfile
│   └── entrypoint.sh
├── cfw
│   └── Dockerfile
├── chttp
│   ├── Dockerfile
│   └── entrypoint.sh
├── cssh
│   ├── Dockerfile
│   └── entrypoint.sh
└── docker-compose.yml

Files

chttp/entrypoint.sh

vim chttp/entrypoint.sh

#! /bin/sh
  
echo "Waiting 2 seconds for router"
sleep 2
ip route add 192.168.101.0/24 via 192.168.102.10 || true

exec "$@"

chttp/Dockerfile

vim chttp/Dockerfile
FROM httpd:2.4-alpine
LABEL Maintainer "John Smith <john@stormforce.ac>"

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

CMD ["httpd-foreground"]

docker-compose.yml

vim docker-compose.yml
......
  four:
    build: chttp
    image: chttp
    container_name: four
    hostname: four
    depends_on:
      - three
    cap_add:
      - ALL
    networks:
      net2:
        ipv4_address: 192.168.102.14
.......

Generare il Gnome terminal four.

Prova intermedia

Partenza del progetto:

docker-compose up

Aprire il terminale one.

Provare il collegamento al server HTTP:

curl -v http://192.168.102.14

Dovrebbe collegarsi.

Provare il collegamento al server SSH:

ssh pippo@192.168.102.12

Dovrebbe funzionare.

Firewall

Decidiamo, per sicurezza, che dall’esterno della rete 192.168.102.0/24 si possa raggiungere solo il server SSH, non quindi il server HTTP.

Occorre usare iptables su cfw e introdurre regole:

  • tutti i collegamenti sono di default proibiti
  • i collegamenti in corso possono continuare
  • è consentito un nuovo collegamento alla porta SSH di cssh

Anche questo è realizzato con un entrypoint.sh, ricostruendo l’immagine cfw.

cfw/entrypoint.sh

vim cfw/entrypoint.sh
#! /bin/sh

# tutti i collegamenti sono di default proibiti  
iptables -P FORWARD DROP
# i collegamenti in corso possono continuare
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
# è consentito un nuovo collegamento alla porta SSH
iptables -A FORWARD -d 192.168.102.0/24 -p tcp --dport 22 -m state --state NEW -j ACCEPT

# ritorna successo anche se vi sono stati degli errori
true

exec "$@"

cfw/Dockerfile

vim cfw/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>

RUN apk --update add --no-cache openssh tcpdump curl iptables

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/bin/sleep","1000000"]

Prova Finale

Rimuovere la vecchia immagine di cfw:

docker rmi cfw:latest

Lanciare il progetto:

docker-compose up

Aprire il terminale one.

Provare il collegamento al server HTTP:

curl -v http://192.168.102.14

Non dovrebbe collegarsi.

Provare il collegamento al server SSH:

ssh pippo@192.168.102.12

Dovrebbe sempre funzionare.


Esercizio 4A

Due reti, server HTTP su seconda rete.

Tunnel da client HTTP a server HTTP via server SSH.

Net04a

Chiunque si collega ad un server SSH può aprire un tunnel ad un altro server.

Da one:

ssh -L 1111:192.168.102.14:80 pippo@192.168.102.12

Questo crea un tunnel dalla porta locale 1111 alla porta 80 della macchina 192.168.102.14.

In un altro tab di terminale dare il comando:

curl -v http://localhost:1111

Si collega al server HTTP remoto.

Questo è un baco di sicurezza.

Tappare il Baco

Nella terminologia di sicurezza si distingue tra Vulnerabilità ed Esposizioni:

  • Vulnerabilità: debolezza intrinseca del software
  • Esposizione: cattiva configurazione (o comportamento improprio)

Questa è una Esposizione.

Nel file di configurazione /etc/ssh/sshd_config trovare e modificare il parametro:

AllowTcpForwarding no

Naturalmente in un Dockerfile questo viene fatto con giudiziosa applicazione del comando sed.

Esercizio 5: Postgres server e client

Obiettivi

L'obiettivo è di installare un server del DB PostgreSQL ed un client grafico da cui accedere al DBMS.

Un'attenta valutazione dei prodotti e delle versioni ci porta a selezionare le immagini docker da usare:

  • postgres:12.2 - versione specifica del server, determinata forse dai requisiti di progetto e di cui conosciamo bene la configurazione e amministrazione
  • dpage/pgadmin4 - client con interfaccia HTML

Server

Variabili d'ambiente

L'esatta configurazione del container del server è guidata da una serie di variabili d'ambiente che devono essere fornite all'atto del run del container:

  • POSTGRES_USER - Utente creato con poteri di superuser, e suo DB
    • Default: postgres
  • POSTGRES_PASSWORD - Necessario sempre
  • POSTGRES_DB - DB a cui connettersi. Default: $POSTGRES_USER
  • POSTGRES_INITDB_ARGS - Se la datadir è inizializzata, argomenti e opzioni per initdb
  • POSTGRES_INITDB_WALDIR - Locazione del Write Ahead Log se diversa dalla datadir
  • POSTGRES_HOST_AUTH_METHOD - Popola pg_hda.conf .
    • Default "host all all all md5"
  • PGDATA - Locazione della datadir.
    • Default: /var/lib/postgresql/data

Persistenza dei dati

Mappare la datadir container a una directory host:

-v "$HOME/pg/datadir":/var/lib/postgresql/data

Altrimenti tutti i database spariscono quando il container è rimosso

In questa versione tutti i file di configurazione sono dentro la datadir.

Un'alternativa può essere la creazione di un volume docker e mappaggio della datadir a questo volume.

Port publishing

Per vedere la porta Postgres dalla rete dello host:

-p 5432:5432

(oppure p.es. -p 15432:5432 se c’è già postgres attivo sullo host)

Non deve esserci se il servizio Postgres è solo visto dalla rete interna.

Rete interna

Altri container vedono il container server postgres:

--net postnet --name postserver

Richiede previa creazione:

docker network create postnet

Lancio del server

Scaricare l’immagine:

docker pull postgres:12.2

Verifica:

docker images

Costruzione della directory dati locale:

mkdir -p $HOME/pg

Creazione della rete:

docker network create postnet

Start del server:

docker run -d --net postnet --name postserver \
 -v "$HOME/pg/datadir":/var/lib/postgresql/data \
 -e POSTGRES_PASSWORD="secret" \
 postgres:12.2

Se la datadir è vuota il container la inizializza con initdb. Se è piena, la usa.

Client Postgres semplice

Con un contenitore derivato dalla stessa immagine:

docker run -ti --rm --net postnet --name client \
postgres:12.2 psql -h postserver -U postgres
  • Senza comando parte il server
  • Con il comando parte il client
  • Viene chiesta la password dell’utente ‘postgres’, ‘secret’ in questo esempio
  • Attenzione: quando si dà ‘\q’ il contenitore viene chiuso e rimosso automaticamente

E’ anche possibile lanciare una shell:

docker run -ti --rm --net postnet --name postclient  \
-v "$HOME/pg/etc":/etc/postgresql  \
postgres:12.2 /bin/bash

Per uscire temporaneamente a linea di comando:

Ctrl-P Ctrl-Q

Per tornare al contenitore:

docker attach client

Stiamo condividendo una directory per eventualmente trasferire dati dallo host.

PgAdmin 4

Client grafico con interfaccia web. Dettagli su: https://www.pgadmin.org/docs/pgadmin4/4.21/container_deployment.html

Pull dell’immagine:

docker pull dpage/pgadmin4

Lancio:

docker run -d --net postnet --name pgadmin -p 8080:80 \
-e 'PGADMIN_DEFAULT_EMAIL=mich@stormforce.ac' \
-e 'PGADMIN_DEFAULT_PASSWORD=supersecret' \
-e 'PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION=True' \
-e 'PGADMIN_CONFIG_LOGIN_BANNER="Authorised users only!"' \
-e 'PGADMIN_CONFIG_CONSOLE_LOG_LEVEL=10' \
dpage/pgadmin4

Collegarsi con un browser a http://localhost:8080/

Esercizio con Docker Compose

postserver e pgadmin sono due microservizi, collegati dalla rete postnet, che offre anche risoluzione nomi-indirizzi.

Postnet

Scriviamo il file di specifiche:

mkdir -p $HOME/pg
cd $HOME/pg
vi docker-compose.yml
version: '3.6'

services:
  postserver:
    image: postgres:12.2
    environment:
      POSTGRES_PASSWORD: "secret"
    ports:
      - "5432:5432"
    volumes:
      - /home/mich/pg/datadir:/var/lib/postgresql/data
    networks:
      postnet:
        ipv4_address: 192.168.100.2


  pgadmin:
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: "mich@stormforce.ac"
      PGADMIN_DEFAULT_PASSWORD: "supersecret"
    ports:
      - "8000:80"
    depends_on:
      - postserver
    networks:
      postnet:
        ipv4_address: 192.168.100.3

networks:
  postnet:
    name: postnet
    ipam:
      driver: default
      config:
        - subnet: 192.168.100.0/24

Test di debugging:

docker compose up

Si vedrà sui log su console la creazione della datadir.

Quando funziona:

Ctrl-C

Quindi:

docker compose down
docker compose up -d

Provare il collegamento a localhost:8000

Docker Swarm

Subito una chiarificazione: esistono in Docker due tipi di Swarm:

  • Docker Swarm (vecchio) - plugin aggiuntivo a Docker base, come parte dell'offerta enterprise. E' obsoleto e non più supportato.
  • Swarm Mode - funzionalità integrata nel Docker base moderno. Non occorre alcun plugin o licenza aggiuntiva.

Swarm Mode è basato su un cluster, i cui nodi possono essere ovunque in una rete visibile a ciascun nodo. E' una modalità di Orchestrazione Remota, da confrontarsi con Docker Compose che è uno strumento di Orchestrazione Locale.

Lo scopo di utilizzo di uno Swarm è di gestire su di esso servizi, con le seguenti proprietà:

  • scalabilità - un servizio può essere deployed su più nodi del cluster
  • bilanciamento del carico - accessi diversi sono a istanze diverse del servizio
  • auto-riparazione - se un container fallisce, viene rigenerato; se un nodo fallisce, i suoi task sono re-istanziati su altri nodi

Terminologia

Un nodo è un'istanza di Docker Engine che partecipa ad uno swarm.

Vi sono due tipi di nodi:

  • managers - controllano lo swarm ed allocano i task ai nodi
  • workers - eseguono i task implementando i servizi

Un task è un container docker con i comandi eseguiti all'interno del container. Un task è allocato ad un nodo da uno dei manager.

Un servizio è un insieme di uno o più task uguali allocato a uno o più nodi e gestito come singola unità verso l'esterno del cluster.

Macchine Virtuali

Si può creare un cluster Swarm con un cero numero di macchine virtuali. Questa è la soluzione più intuitiva, ma richiede un periodo di preparazione lungo: provisioning manuale.

Il nostro progetto è dato dal diagramma.

Vmclust

Componenti

  • 3 servers con Ubuntu Server su VirtualBox
  • 1.5 GB RAM
  • connessione NAT a internet tramite host
  • Port Forwarding per accedere da host
  • connessione a rete interna
  • Docker installato

Steps di preparazione

  1. Installazione di Ubuntu Server

  2. Configurazione di Port Forwarding di VirtualBox

  3. Login senza password

  4. sudo senza password

  5. Collegamento da Gnome Terminal

  6. Clonare altre VM e modificarne

  • hostname
  • indirizzo IP

Installazione di Ubuntu Server

Ubuntu Server:

  • E’ supportato da Docker
  • E’ relativamente veloce nel boot e shutdown
  • Non ha grafica
    • Cut & Paste da terminale collegato in SSH

Installare la prima VM

  • Settings: Adapter 1 in NAT, Adapter 2 su Internal network
  • Seguire la procedura di installazione
  • Lo hostname è gandalf
  • Indirizzo IP su [eth1]: 192.168.10.2/24
  • Utente john, password ‘password’
  • Togliere richiesta di password a sudo

Installare Docker

sudo apt install docker.io

Test: pull di un’immagine Alpine

docker pull alpine

Port Forwarding da host a VM

Per la VM gandalf, dal manager VirtualBox:

  • Aprire i Settings
  • Andare su Networks -> Adapter 1
  • Espandere Advanced
  • Cliccare su Port Forwarding

Port Forwarding

  • Salvare

Login senza password

Vogliamo compiere il login alle VM senza dover dare la password

Prima attivare la macchina virtuale e collegarsi (Esempio: gandalf, con porta 5682)

  • Da Host, collegarsi:
ssh john@127.0.0.1 -p 5682
  • Accettare la chiave crittografica
  • Dare la password
  • Scollegarsi

Ora installare l’utility sshpass:

sudo apt install sshpass

Creare un file con la password:

echo "password" > ~/.sshpass

Il collegamento è ora:

sshpass -f ~/.sshpass ssh john@127.0.0.1 -p 5682

sudo senza password

Assicurarsi prima che root abbia una password:

sudo passwd root

Modificare /etc/sudoers su gandalf:

%sudo ALL=(ALL:ALL) NOPASSWD:ALL

In vi va salvato con :wq!

  • NON SBAGLIARE

Logout da terminale e login di nuovo

Collegamento da Gnome Terminal

Nel terminale aprire le Preferenze:

Nel tab Command inserire il Custom command:

bash -c "sshpass -f ~/.sshpass ssh john@127.0.0.1 -p 5682"

Eventualmente cambiare colori, dimensioni, ecc.

Gnome Terminal

Altre VM

Le altre VM sono clonate dalla prima

Modifiche, una per una, poi reboot

  • Configurare il Port Forwarding al NAT prima del boot
  • Settare il nome in /etc/hostname
  • Settare gli indirizzi in /etc/netplan/00-installer-config.yaml

Start di gollum, da solo (gandalf non deve essere attivo)

Modifiche ai file: /etc/hostname

sostituire gandalf con gollum

Modifiche al file /etc/netplan/00-installer-config.yaml che deve diventare:

network:
  ethernets:
    enp0s3:
      dhcp4: true
    enp0s8:
      addresses:
      - 192.168.10.3/24
      nameservers:
        addresses: []
        search: []
  version: 2

Reboot

Clonare anche la VM grendel (192.168.10.4) ed apportare modifiche simili

Generare opportuni Gnome Terminal per gollum e grendel

Le tre VM collegate

Tre VM

Porte usate da Swarm

Le porte seguenti devono essere accessibili sui nodi dello Swarm. In qualche versione di sistema operativo sono aperte di default.

  • 2377 TCP - per comunicazioni con i nodi manager
  • 7946 TCP/UDP - per la scoperta dei nodi sulla rete overlay
  • 4789 UDP - per il traffico sulla rete overlay

Il traffico sulla porta 4789 UDP, altrimenti detta porta VXLAN, non ha autenticazione. E' importante usare una rete intrinsecamente sicura. L'alternativa, non considerata qui, è di creare una rete overlay crittata, utilizzando IPSec ESP.

Se le porte non sono aperte, p.es. su sistemi Red Hat, Oracle o CentOS, su ogni nodo eseguire:

firewall-cmd --add-port=2376/tcp --permanent
firewall-cmd --add-port=2377/tcp --permanent
firewall-cmd --add-port=7946/tcp --permanent
firewall-cmd --add-port=7946/udp --permanent
firewall-cmd --add-port=4789/udp --permanent
firewall-cmd --reload

Quindi

systemctl restart docker

In caso di errori

firewall-cmd --remove-port=port-number/tcp —permanent.

Porte su Ubuntu e simili

Anche sui sistemi Debian occorre assicurarsi che le porte necessarie siano aperte.

Su ogni nodo dare il comando:

sudo iptables -A INPUT -p tcp --dport 2376 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 2377 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 7946 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 7946 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 4789 -j ACCEPT
sudo iptables -A INPUT -p esp -j ACCEPT

In particolare la porta 7946 viene usata dalla rete Ingress, e permette la visibilità all'esterno dei servizi del cluster.

Creazione di Swarm

Occorre decidere il nodo manager. Per noi sarà gandalf (IP: 192.168.10.2).

Inizializzazione dello Swarm

Su gandalf:

docker swarm init --advertise-addr 192.168.10.2

Un output possibile è:

Swarm initialized: current node (m06bd3ak4olhz6uwfyou0bevf) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1fw3gizof8asovcxnfxe7c1ry8e356psk4jxlntp5hcggmz3pl-1r3gejplppoijm78gcb80j5ms 192.168.10.2:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Verificare.

docker info
.......
Swarm: active
  NodeID: m06bd3ak4olhz6uwfyou0bevf
  Is Manager: true
  ClusterID: sqsz2f7f0gaens0ipqohgji4e
  Managers: 1
  Nodes: 1
.......

Lista dei nodi:

docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
m06bd3ak4olhz6uwfyou0bevf *   gandalf             Ready               Active              Leader              19.03.8

Per rivedere il comando da usare sui workers che vogliono unirsi allo swarm, dare sul manager (gandalf):

docker swarm join-token worker
To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1fw3gizof8asovcxnfxe7c1ry8e356psk4jxlntp5hcggmz3pl-1r3gejplppoijm78gcb80j5ms 192.168.10.2:2377

Connettere nodi worker allo Swarm

Su gollum prima, poi su grendel, dare il comando:

docker swarm join --token SWMTKN-1-1fw3gizof8asovcxnfxe7c1ry8e356psk4jxlntp5hcggmz3pl-1r3gejplppoijm78gcb80j5ms 192.168.10.2:2377

Il risultato dovrebbe essere:

This node joined a swarm as a worker.

Sul manager (gandalf) verificare la lista dei nodi:

docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
m06bd3ak4olhz6uwfyou0bevf *   gandalf             Ready               Active              Leader              19.03.8
ydj4e8ffjgxmwe6i683iyu4ea     gollum              Ready               Active                                  19.03.8
u4kpazamccxy186bn9kttr1nu     grendel             Ready               Active                                  19.03.8

Swarm con Nodi Docker

Oltre che con macchine virtuali è possibile implementare un cluster Swarm con contenitori Docker.

La soluzione è molto più leggera.

sw-swarm

Preparazione

Immagine Docker

Un'immagine che esegua Docker non è difficile da costruire, e può essere basata sul'immagine di base Alpine.

Preparazione del progetto:

mkdir -p ~/swarm/doker
cd ~/swarm/doker

La nostra immagine si chiamerà doker.

Oltre che il Docker Engine vi saranno altre utilities.

Sarà inoltre un server SSH, con il permesso di login a root, con password root. E' previsto anche un utente john con password password.

Un entrypoint configurerà le regole di firewall necessarie a Swarm.

Inoltre lo entrypoint lancerà il server SSH in modalità demone. Avremo quindi un'immagine con due processi attivi: dockerd come processo 1 e SSH come altro processo.

Certificato

C'è inoltre bisogno di predisporre l'immagine con un certificato per il pseudo-hostname myregistry.com. Questo consentirà l'uso dai nodi dello swarm di un servizio registry locale.

Generare il certificato con:

openssl req -newkey rsa:4096 -nodes -sha256\
  -keyout registry.key -x509 -days 365\
  -out registry.crt -subj "/CN=myregistry.com"\
  -addext "subjectAltName = DNS:myregistry.com"

Questo genera due files:

  • registry.crt - il certificato
  • registry.key - la chiave privata

Il Dockerfile

Prepariamo il Dockerfile:

vi Dockerfile
FROM alpine

# Add software
RUN apk add --no-cache docker git jq openssl \
    shadow ncurses

RUN apk --update add --no-cache openssh tcpdump bash curl \
  && rm -rf /var/cache/apk/*

# ‘root’ può fare login
RUN sed -i s/#PermitRootLogin.*/PermitRootLogin\ yes/ /etc/ssh/sshd_config \
  && echo "root:root" | chpasswd
# Abilitare la porta 22
RUN sed -ie 's/#Port 22/Port 22/g' /etc/ssh/sshd_config

# Abilitare le chiavi crittografiche
RUN sed -ri 's/#HostKey \/etc\/ssh\/ssh_host_key/HostKey \/etc\/ssh\/ssh_host_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_rsa_key/HostKey \/etc\/ssh\/ssh_host_rsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_dsa_key/HostKey \/etc\/ssh\/ssh_host_dsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ecdsa_key/HostKey \/etc\/ssh\/ssh_host_ecdsa_key/g' /etc/ssh/sshd_config
RUN sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ed25519_key/HostKey \/etc\/ssh\/ssh_host_ed25519_key/g' /etc/ssh/sshd_config

# Generare le chiavi crittografiche
RUN /usr/bin/ssh-keygen -A
RUN ssh-keygen -t rsa -b 4096 -f  /etc/ssh/ssh_host_key

# Generazione dell’utente ‘john’ con password ‘password’
RUN adduser -D john && echo "john:password" | chpasswd

# Porta da esporre al port mapping
EXPOSE 22

# Certificati per il registry
COPY registry.crt /root
COPY registry.key /root
RUN mkdir -p /etc/docker/certs.d/myregistry.com:5000
COPY registry.crt /etc/docker/certs.d/myregistry.com:5000/ca.crt

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["dockerd"]

Lo entrypoint è:

vi entrypoint.sh
#! /bin/sh

# Porte necessarie a Swarm
iptables -A INPUT -p tcp --dport 2377 -j ACCEPT
iptables -A INPUT -p tcp --dport 7946 -j ACCEPT
iptables -A INPUT -p udp --dport 7946 -j ACCEPT
iptables -A INPUT -p udp --dport 4789 -j ACCEPT
iptables -A INPUT -p esp -j ACCEPT

# Lancio del server SSH
/usr/sbin/sshd

# Aggiungere la risoluzione del registry
echo "192.168.77.2 myregistry.com" >> /etc/hosts

exec "$@"

Si presume che la rete su cui girerà lo swarm sia la 192.168.77.0/24.

L'immagine doker viene costruita col comando:

docker build -t doker .

Procedure shell per il cluster

Posizionamento nella directory di progetto:

cd ~/swarm

Procedura di Setup

Procedura di setup del cluster Swarm:

vi startcluster.sh
# Crea la rete su cui porre i nodi dello swarm
if docker network ls | grep swarmnet; then
  :
else
  docker network create swarmnet --subnet 192.168.77.0/24
fi

# Ogni nodo ha uno Host Mapping per preservare le immagini precedentemente scaricate
# un secondo host mapping per comunicare col registry su host
# terzo host mapping per ricevere files da host
# e un quarto per il mapping di dati persistenti
echo "Creating manager"
docker run -d --privileged --name manager --hostname manager --net swarmnet \
  -v $PWD/manager:/var/lib/docker \
  -v ~/registry:/registry \
  -v ~/manager-tmp:/tmp \
  -v ~/data:/data \
  doker
echo "Initialising manager"
docker exec -ti manager docker swarm init

# Il token di join
SWARM_TOKEN=$(docker exec -ti manager docker swarm join-token -q worker)
# I messaggi di Swarm sono in formato MSDOS
# E' necessario togliere il Carriage Return
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
echo Swarm token is $SWARM_TOKEN

# IP del master
SWARM_MASTER_IP=$(docker exec -ti manager docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
echo Swarm master IP is $SWARM_MASTER_IP

# Numero di workers
NUM_WORKERS=3
echo There will be $NUM_WORKERS workers

# Generare i workers e aggiungerli allo swarm
for i in $(seq "$NUM_WORKERS"); do
  echo "Starting worker-${i}"
  docker run -d --privileged --name worker-${i} --hostname worker-${i} \
    --net swarmnet \
    -v $PWD/worker-${i}:/var/lib/docker \
    -v ~/data:/data \
    doker
  echo "Joining worker-${i}"
#  sleep 8
  docker exec -ti worker-${i} docker swarm join \
    --token $SWARM_TOKEN $SWARM_MASTER_IP:2377
done

docker exec -ti manager docker node ls

Il nodo master si chiama manager. I nodi worker si chiamano worker-1, worker-2, ecc.

La procedura viene resa eseguibile:

chmod +x startcluster.sh

Procedura di Teardown

Per chiudere in maniera adeguata lo Swarm e rimuoverne i nodi.

vi stopcluster.sh
# Rimozione dei nodi worker
echo "Removing worker nodes"
for i in $(docker ps -a | grep worker | awk '{n=NF;print $n}'); do
  echo "Node $i leaves swarm"
  docker exec -ti $i docker swarm leave -f
done

echo "Nodes are removed"
docker rm -f $(docker ps -a | grep worker | awk '{n=NF;print $n}')

# Rimozione del manager
echo "Master leaves swarm"
docker exec -ti manager docker swarm leave -f

echo "Removing manager"
docker rm -f manager

echo "Swarm cluster deleted"

Renderla eseguibile:

chmod +x stopcluster.sh

Lancio di Swarm

Semplicemente, nella directory ~/swarm, è l'esecuzione di:

./startcluster.sh

Con risultato simile a:

Creating manager
6beab45536ff982b109ef279079cb5a3029f242838620f9debcfe3fcd42b029b
Initialising manager
Swarm initialized: current node (up1038772l2darfkf4i9yot95) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-4jmp9d1eng04ks5go2h5owgo44zncb2uurapk1fr6fctzzpq40-4yvvbnndi57kzgpqcjf13iqzk 172.17.0.2:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Swarm token is SWMTKN-1-4jmp9d1eng04ks5go2h5owgo44zncb2uurapk1fr6fctzzpq40-4yvvbnndi57kzgpqcjf13iqzk
Swarm master IP is 172.17.0.2
There will be 3 workers
Starting worker-1
168918df8ec8e043f5572a510f69d3f826f64dcb1f53bf1b4385051e38fc5e38
Joining worker-1
This node joined a swarm as a worker.
Starting worker-2
064efbacc8b69c565639093eed23edb2e3e41091ed6b889890f0e28839619066
Joining worker-2
This node joined a swarm as a worker.
Starting worker-3
913731b95277e689702a64555b06611f0a021a1a1a2dd5d4054eea1f78b370db
Joining worker-3
This node joined a swarm as a worker.
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
up1038772l2darfkf4i9yot95 *   manager    Ready     Active         Leader           25.0.5
3ju2p5efck7qcgze14zfp3jmw     worker-1   Ready     Active                          25.0.5
zbdg3ciulaeyt8he28v2uu8v9     worker-2   Ready     Active                          25.0.5
p9e5hpqcijlrb9048lplhg4gy     worker-3   Ready     Active                          25.0.5

Nota

Nella configurazione corrente tutti i comandi devono essere eseguiti sul container della macchina ospite che si chiama manager.

Conviene creare un alias per risparmiare digitazione:

alias dk='docker exec -ti manager'

Con tale alias la lista dei nodi si ottiene con:

dk docker node ls

Gestione dei Nodi

Aggiunta di nodi worker

Un nodo aggiuntivo deve

  • essere creato come contenitore docker
  • dare il comando di join allo swarm

Creazione di nodo

Per esempio, per aggiungere il nodo worker-7, col comando

docker run -d --privileged \
  --name worker-7 --hostname worker-7 --net swarmnet \
  -v $PWD/worker-7:/var/lib/docker \
  doker

Occorre l'opzione --privileged affinchè il nodo possa settare le regole di iptables.

E' una buona idea lo host mapping della directory di docker sul nodo ad una directory sulla macchina locale: il pull di immagini già esistenti non viene effettuato.

Join allo swarm

Il token di join può essere chiesto al manager:

docker exec -ti manager docker swarm join-token -q worker

Attenzione che il risultato è una stringa in formato MSDOS, quindi terminata da CR+NL anzichè da solo NL come in Unix.

Per rimuovere il CR e trasformare la stringa in formato Unix:

docker exec -ti manager docker swarm join-token -q worker |tr -d "\r" 

Quindi per aggiungere il nodo, p.es. worker-7 allo swarm, nella nostra configurazione:

SWARM_TOKEN=$(docker exec -ti manager docker swarm join-token -q worker)
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
SWARM_MASTER_IP=$(docker exec -ti manager docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
docker exec -ti worker-7 docker swarm join \
  --token $SWARM_TOKEN $SWARM_MASTER_IP:2377

Procedura shell di aggiunta nodi

Aggiunta di un singolo nodo

Aggiungere un singolo nodo con nome.

vi addnode.sh
# Controllare l'esistenza del manager
if docker ps | grep manager; then
  :
else
  echo "Node 'manager' does not exist"
  echo "Create swarm cluster first"
  exit 1
fi

if [ $# -lt 1 ]; then
  echo "Must provide name of worker"
  echo "Syntax: $0 worker"
  exit 2
fi

WORKER=$1

# Il token di join
SWARM_TOKEN=$(docker exec -ti manager docker swarm join-token -q worker)
# I messaggi di Swarm sono in formato MSDOS
# E' necessario togliere il Carriage Return
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
echo Swarm token is $SWARM_TOKEN

# IP del master
SWARM_MASTER_IP=$(docker exec -ti manager docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
echo Swarm master IP is $SWARM_MASTER_IP

# Generare il workers e aggiungerlo allo swarm
echo "Starting $WORKER"
docker run -d --privileged \
  --name $WORKER --hostname $WORKER --net swarmnet \
  -v $PWD/$WORKER:/var/lib/docker \
  -v ~/data:/data \
  doker
  echo "Joining $WORKER"
sleep 5
docker exec -ti $WORKER docker swarm join \
  --token $SWARM_TOKEN $SWARM_MASTER_IP:2377

docker exec -ti manager docker node ls
chmode +x addnode.sh

Aggiunta di più nodi

La seguente procedura shell permette di aggiungere un numero di nodi allo swarm:

vi newnodes.sh
# Controllare l'esistenza del manager
if docker ps | grep manager; then
  :
else
  echo "Node 'manager' does not exist"
  echo "Create swarm cluster first"
  exit 1
fi

# Il token di join
SWARM_TOKEN=$(docker exec -ti manager docker swarm join-token -q worker)
# I messaggi di Swarm sono in formato MSDOS
# E' necessario togliere il Carriage Return
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
echo Swarm token is $SWARM_TOKEN

# IP del master
SWARM_MASTER_IP=$(docker exec -ti manager docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
echo Swarm master IP is $SWARM_MASTER_IP

# Numero di workers
if  (($# < 1)) ; then
  echo "Must provide number of nodes to add"
  echo "Syntax: $0 numnodes"
  exit 2
fi
NUM_WORKERS=$1
LAST_WORKER=$(docker ps | grep worker | awk '{n=NF; print $n}' \
  | sort | tail -1 | cut -d- -f2)
NEXT_WORKER=$( expr $LAST_WORKER + 1 )
LAST_ADDED=$( expr $NEXT_WORKER + $NUM_WORKERS - 1 )
echo $NUM_WORKERS workers will be added, from $NEXT_WORKER to $LAST_ADDED

# Generare i workers e aggiungerli allo swarm
for i in $(seq "$NEXT_WORKER" "$LAST_ADDED"); do
  echo "Starting worker-${i}"
  docker run -d --privileged \
    --name worker-${i} --hostname worker-${i} --net swarmnet \
    -v $PWD/worker-${i}:/var/lib/docker \
    -v ~/data:/data \
    doker
  echo "Joining worker-${i}"
  sleep 3
  docker exec -ti worker-${i} docker swarm join \
    --token $SWARM_TOKEN $SWARM_MASTER_IP:2377
done

docker exec -ti manager docker node ls

Renderla eseguibile:

chmod +x newnodes.sh

Eseguirla col comando, per esempio per aggiungere 3 nodi:

./newnodes.sh 3

Rimozione di nodo

Il nodo da il comando per lasciare lo swarm.

Per esempio per worker-7:

docker exec -ti worker-7 docker swarm leave

Il nodo lascia lo swarm.

Attenzione che il nodo rimane però nella tabella dei nodi del manager. Infatti:

dk docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
.....
aj0ope2gl1u90dptwv71fo7ta     worker-7   Down      Active                          26.1.5

Lo STATUS del nodo è marcato come Down.

Per evitare eccessiva digitazione è stato creato l'alias:

alias dk='docker exec -ti manager'

Il contenitore docker worker-7 naturalmente continua ad esistere.

Per rimuoverlo dalla tabella del manager occorre dare il comando:

dk docker node rm worker-7

Se si compie un nuovo join del nodo worker-7 allo swarm senza averlo rimosso dalla tabella dei nodi del manager:

SWARM_TOKEN=$(docker exec -ti manager docker swarm join-token -q worker)
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
SWARM_MASTER_IP=$(docker exec -ti manager docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
docker exec -ti worker-7 docker swarm join \
  --token $SWARM_TOKEN $SWARM_MASTER_IP:2377

La procedura funziona ma viene creata un'altra istanza di nodo in tabella, e quella vecchia, Down, continua ad esistere.

dk docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
.....
8m9lhsotqmb2wn84m1t21k1qr     worker-7   Ready     Active                          26.1.5
aj0ope2gl1u90dptwv71fo7ta     worker-7   Down      Active                          26.1.5

A questo punto non si può rimuoverla col nome, ma solo tramite l'identificativo:

dk docker node rm aj0ope2gl1u90dptwv71fo7ta

Procedura di rimozione

Rimuovere un nodo e rimuovere il contenitore docker dalla macchina ospite:

vi rmnode.sh
# Controllare la sintassi, un argomento è necessario
if [ $# -lt 1 ]; then
  echo "Must indicate node to remove"
  echo "Syntax: $0 node"
  exit 1
fi
NODE=$1

# Controllare che il nodo esista
if docker exec -ti manager docker node ls | grep $NODE > /dev/null 2>&1
then
  :
else
  echo "Node $NODE does not exist in cluster"
  exit 2
fi

# Il nodo lascia lo swarm
docker exec -ti $NODE docker swarm leave
# Attendere la rimozione dalla tabella dei nodi
echo "Waiting for $NODE to be down"
while true; do
  if docker exec -ti manager docker node ls | grep $NODE | grep Down ; then
    break
  else
    sleep 1
    continue
  fi
done
echo "Node $NODE is down"

# Il nodo è cancellato dalla tabella del manager
echo "Removing $NODE from manager table"""
docker exec -ti manager docker node rm $NODE

# Rimuovere il contenitore dalla macchina ospite
docker rm -f $NODE
echo "Node $NODE removed from host docker"

Renderla eseguibile:

chmod +x rmnode.sh

Promozione e demozione di nodo

Promuovere nodi a manager, esempi:

dk docker node promote worker-1
dk docker node promote worker-2
dk docker node promote worker-3

Per degradare (demuovere) un nodo da manager a worker il comando è, p-es.:

dk docker node demote worker-3

Lo stato dello swarm è ora:

dk docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
q2v80vqnl99wmuom4z784gvvs *   manager    Ready     Active         Leader           26.1.5
iggo7d5z82jb8zohjoxl093fq     worker-1   Ready     Active         Reachable        26.1.5
fcj1b9vc16z2corput5ly0d1t     worker-2   Ready     Active         Reachable        26.1.5
jexh3jjpysvs7fz0h8k93n7db     worker-3   Ready     Active                          26.1.5
kn7q5tcb7b0844613xhwb30hi     worker-4   Ready     
.....

Ora un manager può lasciare lo swarm:

dk docker swarm leave -f

Il nodo manager ha lasciato lo swarm. L'opzione -f è indispensabile per forzare l'uscita di un nodo manager.

Il cluster elegge un nuovo leader. Si possono ora indirizzare comandi di gestione del cluster ad un manager qualsiasi, per esempio worker-1.

La nuova tabella del cluster è:

docker exec -ti worker-1 docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
q2v80vqnl99wmuom4z784gvvs     manager    Down      Active         Unreachable      26.1.5
iggo7d5z82jb8zohjoxl093fq *   worker-1   Ready     Active         Reachable        26.1.5
fcj1b9vc16z2corput5ly0d1t     worker-2   Ready     Active         Leader           26.1.5
jexh3jjpysvs7fz0h8k93n7db     worker-3   Ready     Active                          26.1.5
kn7q5tcb7b0844613xhwb30hi     worker-4   Ready     Active                          26.1.5
.....

Si nota che worker-2 è stato eletto nuovo leader del cluster.

I vecchi manager nello stato di Down devono essere degradati a worker prima di essere rimossi dalla tabella del cluster:

docker exec -ti worker-1  docker node demote manager
docker exec -ti worker-1  docker node rm manager

Se vi sono duplicazioni usare lo ID anzichè lo HOSTNAME.

L'elezione di un nuovo leader avviene con il protocollo di consenso RAFT.

Il leader è il manager primario, che compie tutte le decisioni di amministrazione del cluster e di orchestrazione dei task.

Per avere un cluster stabile è necessario avere un numero dispari di manager: 1 (minimo), 3, 5 o 7 (massimo raccomandato).

Quando viene meno un manager, si raccomanda di portare al più presto il numero dei manager al prossimo numero dispari.

Questo si può fare:

  • promuovendo un worker a manager
  • istanziando un nuovo nodo con il token di join dei master

Per conoscere il token di join come manager, chiedere ad un maneger corrente qualsiasi:

SWARM_TOKEN=$(docker exec -ti worker-1 docker swarm join-token -q manager)
SWARM_TOKEN=$(echo $SWARM_TOKEN | tr -d "\r")
echo "Swarm Token is $SWARM_TOKEN"

Occorre anche l'indirizzo IP di uno dei manager attivi.

SWARM_MASTER_IP=$(docker exec -ti worker-1 docker info \
  | grep -w "Node Address" | awk '{print $3}')
SWARM_MASTER_IP=$(echo $SWARM_MASTER_IP |tr -d "\r")
echo "Swarm Master IP is $SWARM_MASTER_IP"

Ora possiamo reinstanziare manager come nodo manager del cluster:

docker exec -ti manager docker swarm join \
  --token $SWARM_TOKEN $SWARM_MASTER_IP:2377

Non sarà il leader, ma vi si possono inviare comandi di amministrazione, e saranno ridiretti al leader.

Etichette ad un nodo

Eì possibile aggiungere una label ad un nodo. p.es.:

dk docker node update --label-add function=production worker-5

Una label è una coppia chiave=valore.

Una label può essere usata per restringere l'assegnazione di servizi a determinati nodi, per esempio con l'opzione --constraint node.labels.function=production.

Per vedere le label del nodo worker-5 in formato YAML:

dk docker node inspect worker-5 --pretty

Per rimuovere una label, p.es.:

dk docker node update --label-rm function worker-5

Le label sono maps in linguaggio Go, su cui Docker è costruito.

Per vedere tutte le label di ogni nodo manca un comando specifico, ma si può estrarre l'informazione con:

dk bash -c "docker node ls -q | xargs docker node inspect \
  -f '{{ .Description.Hostname }}: {{ .Spec.Labels }}'"

Disponibilità un nodo

Vi sono tre stati di disponibilità di un nodo:

  • active
  • pause
  • drain

Si può mettere un nodo in pausa con:

dk docker node update --availability pause worker-4

Non verranno schedulati nuovi task al nodo, ma quelli presenti continuano a funzionare.

Per toglierlo dalla pausa:

dk docker node update --availability active worker-4

Possono venire schedulati nuovi task al nodo.

Si può mettere un nodo nello stato di drain con:

dk docker node update --availability drain worker-4

Non vengono schedulati nuovi task al nodo, e quelli presenti sono gradualmente migrati ad altri nodi.

Servizi in Docker Swarm

Caratteristiche di un Servizio

Il deployment di servizi è lo scopo principale di utilizzo di Docker Swarm.

Un servizio è un certo numero di task implementati su uno o più nodi del cluster.

Il nostro applicativo è tipicamente architettato in una serie di servizi comunicanti e cooperanti per uno scopo definito - Architettura a Microservizi.

Almeno uno dei servizi componenti è visibile dall'esterno del cluster e serve a comunicazioni tra il servizio (interno al cluster) e i clients che usano il servizio (esterni).

In Docker Swarm vi sono due tipi di servizi:

  • Servizi replicati : task che viene eseguito in un numero di repliche predeterminate. Ogni replica è un’istanza del container definito nel servizio.

  • Servizi globali : ogni nodo disponibile nel cluster ha un task per il relativo servizio. Se si aggiunge un nuovo nodo al cluster, lo swarm manager gli assegnerà immediatamente un task di quel servizio.

Deployment di Servizio Replicato

Deployment di servizio replicato allo swarm, con una sola replica:

dk docker service create --name my-nginx --replicas 1 -p 8000:80 nginx

Si può omettere l'opzione --relicas 1: di default un servizio è replicato con una replica.

Viene prodotto un rapporto del tipo:

hefgmk41sne1bjp0y9axrlyvf
overall progress: 0 out of 1 tasks 
1/1: preparing

La stringa è l'identificativo del servizio.

Il servizio è in preparazione e non è ancora pronto, p.es. deve ancora scaricare l'immagine nginx dal Docker Hub.

Si può attendere o premere Ctrl-C: in tal caso la preparazione del servizio procede in background e ritorna il pronto.

Controllare lo stato del servizio con:

dk docker service ps my-nginx

Il risultato è simile a:

ID             NAME         IMAGE          NODE       DESIRED STATE   CURRENT STATE            ERROR     PORTS
w8yl8k492rkj   my-nginx.1   nginx:latest   worker-1   Running         Preparing 27 seconds ago        

Il nodo a cui il servizio è stato assegnato sta ancora compiendo il pull dell'immagine e quindi il suo stato è Preparing.

Dopo un certo periodo di tempo, ripetendo il comando, il risultato è simile a:

ID             NAME         IMAGE          NODE      DESIRED STATE   CURRENT STATE           ERROR     PORTS
ixf8qekp6lz0   my-nginx.1   nginx:latest   worker-1    Running         Running 4 minutes ago

Vincoli al servizio

Di solito un servizio è assegnato ad un nodo casuale dello swarm.

Possiamo desiderare che un determinato servizio venga assegnato ad un certo nodo, o che non venga assegnato ad un nodo.

Possiamo indicarlo specificando dei vincoli (constraints) al lancio del servizio. Per esempio:

dk docker service create --name my-nginx1 --replicas 1 -p 8888:80 \
  --constraint node.hostname!=manager \
  nginx

Viene richiesto di non installare il servizio sul nodo il cui hostname è manager.

Gli operatori accettabili nei constraint sono == (uguale) e != (diverso da).

Servizi diversi devono avere nomi diversi. e non devono esservi collisioni nel port mapping.

Tipici constraints sono:

  • node.id - listato con docker node ls
  • node.hostname - listato con docker node ls
  • node.ip
  • node.role - (manager|worker)
  • node.platform.os - (linux|windows|ecc.)
  • node.platform.arch - (x86_64|arm64|386|etc.)
  • node.labels - vuoto di default

Acccesso al Servizio

Dare il comando:

dk curl localhost:8000

Il risultato è la pagina di benvenuto di Nginx.

Questo avviene poichè nel lancio del servizio avevamo specificato il port mapping -p 8000:80: se nginx ascolata sulla porta 80 del contenitore in cui gira, viene reso accessibile alla porta 8000 a livello del cluster.

Non importa in quale nodo del cluster sia il contenitore che implementa Nginx. Chiedere l'accesso al master causa una redirezione interna a tale nodo.

Si può anche dare tale comando su qualsiasi nodo del cluster, con uguale funzionamento. Ma solitamente si interagisce dall'esterno coi servizi, inviando i comandi al nodo master.

Accesso da browser

Occorre conoscere l'indirizzo IP del nodo manager. Si può scoprire con:

docker exec -ti manager docker info | grep -w "Node Address" | awk '{print $3}'

Nel nostro caso:

192.168.77.2

In caso di presenza di pià manager va bene l'indirizzo IP di uno qualsiasi di essi.

Puntare un browser, sulla macchina host, a: 192.168.77.2:8000.

Ispezione di Servizi

Tutti i Servizi

Listare tutti i servizi con:

dk docker service ls

produce un rapporto del tipo:

ID             NAME       MODE         REPLICAS   IMAGE          PORTS
hefgmk41sne1   my-nginx   replicated   1/1        nginx:latest   *:8000->80/tcp

Singolo Servizio

Ispezionare un singolo servizio:

dk docker service inspect --pretty my-nginx

Produce un rapporto sulle proprietà e lo stato del servizio. Il rapporto è in formato YAML. Senza l'opzione --pretty il rapporto è in formato JSON.

Anche:

dk docker service ps my-nginx
ID             NAME         IMAGE          NODE      DESIRED STATE   CURRENT STATE           ERROR     PORTS
clb3qhx1ffyl   my-nginx.1   nginx:latest   worker-1    Running         Running 8 minutes ago

Da cui vediamo che il task singolo che implementa il servizio è stato assegnato al nodo worker-1.

Per vedere il contenitore che implementa il servizio:

docker exec -ti worker-1 docker ps

Scalare il Servizio

Per passare a 3 task che implementano il servizio:

dk docker service scale my-nginx=3

e il responso immediato è:

my-nginx scaled to 3
overall progress: 1 out of 3 tasks 
1/3: running   
2/3: preparing 
3/3: preparing 

Dopo un breve tempo si può vedere l'avvenuta operazione di scala:

dk docker service ps my-nginx
ID             NAME         IMAGE          NODE           DESIRED STATE   CURRENT STATE             ERROR     PORTS
clb3qhx1ffyl   my-nginx.1   nginx:latest   dockub         Running         Running 21 minutes ago              
alqjhs0zemk0   my-nginx.2   nginx:latest   9dd3a81d7be6   Running         Preparing 2 minutes ago             
tllpd8kuuit6   my-nginx.3   nginx:latest   375e7ceddeb5   Running         Preparing 2 minutes ago

E' possibile scalare un servizio a 0 repliche, p.es.:

dk docker service scale my-nginx=0

Il servizio non viene rimosso, ma vi sono al momento 0 nodi che lo eseguono.

Servizio su nodo specifico

Alle volte desideriamo che il container di un determinato servizio venga installato su uno specifico nodo.

Questo si ottiene con l'uso di constraints (vincoli).

Per esempio desideriamo installare il Visualizer sul nodo manager del cluster.

dk docker service create \
  --name=viz \
  --publish=8080:8080 \
  --constraint=node.hostname==manager \
  --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
  --detach=true \
  dockersamples/visualizer

Verifichiamo il deployment in corso del servizio con:

dk docker service ps viz

e attendiamo che arrivi nello stato di Running.

Notiamo che il container è stato posto sul nodo manager.

Deployment di Servizio Globale

dk docker service create --name redis --mode global   
  --publish 6379:6379 redis

L'opzione --mode global rende il servizio globale.

L'assenza di tale opzione denota di default un servizio replicato con una replica.

La presenza dell'opzione, p.es., --replicas 3 denota un servizio replicato con 3 repliche.

Rimuovere un Servizio

Col comando, p.es.:

dk docker service rm my-nginx

viene rimosso il servizio my-nginx su tuti i nodi che lo stavano eseguendo.

Registry locale

E disponibile l'immagine registry:latest su Docker Hub. Installeremo su manager il servizio registry come servizio, con un contenitore da tale immagine.

Docker permette l'accesso a contenitori esterni col protocollo HTTP (non sicuro) solo da localhost. Ugni altra URL, non locale, deve usare il protocollo HTTPS (sicuro).

Ogni nostro nodo swarm deriva dall'immagine doker, che contiene un certificato per myregistry.com, che risolve al nodo manager.

sw-registry

Le immagini del servizio registry di swarm sono le stesse del registry dello host tramite una catena di host mapping (o bind mapping):

  • la directory /var/lib/registry del servizio è mappata alla directory /registry del nodo swarm che implementa il servixio - che deve essere manager.
  • su manager la directory /registry è mappata alla directory ~/registry dello host
  • il contenitore registry, che è fuori dallo swarm ma nell'ambiente docker dello host, mappa anch'esso la sua directory /var/lib/registry alla diretory ~/registry dello host

Così l'immagine che si chiama localhost:5000/immagine sullo host si chiama myregistry.com:5000/immagine per i nodi dello swarm. E' sufficiente caricare con push un'immagine sul registry dello host perchè sia visibile al servizio registry dello swarm.

Lancio del servizio registry

Col comando:

dk docker service create --name registry --publish=5000:5000 \
  --constraint=node.hostname==manager \
  --mount=type=bind,src=/root,dst=/certs \
  --mount=type=bind,src=/registry,dst=/var/lib/registry\
  -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
  registry:latest

Test

Compiere il pull di un'immagine su manager:

dk docker pull nginx

Aggiungere un tag che punta al registry:

dk docker tag nginx:latest myregistry.com:5000/nginx:latest

Compiere il push al registry locale:

dk docker push myregistry.com:5000/nginx:latest

Lanciare un servizio che carica l'immagine dal registry locale:

dk docker service create --name my-nginx \
  --replicas 1 -p 8000:80 \
  myregistry.com:5000/nginx

Scaliamo il servizio:

dk docker service scale my-nginx=5

La velocità è notevolmente aumentata. Inoltre abbiamo la possibilità di porre nel registry le nostre immagini private.

Esercizio: Sviluppo di un servizio

Preparare lo scaffolding:

mkdir -p ~/swarm/hello
cd ~/swarm/hello

Il nostro applicativo è scritto in Go, è un piccolo web server che stampa un messaggio e lo hostname del nodo su cui esegue.

vi main.go
package main

import (
  "fmt"
  "log"
  "net/http"
  "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
  hostname, _ := os.Hostname()
  fmt.Fprintf(w, "Hello, 世界. My hostname is: %s\n", hostname)
}
func main() {
  http.HandleFunc("/", handler)
  fmt.Println("Running demo app. Press Ctrl+C to exit...")
  log.Fatal(http.ListenAndServe(":8888", nil))
}

Il Dockerfile di generazione dell'immagine è:

FROM golang:1.17-alpine AS build

WORKDIR /src/
COPY main.go go.* /src/
ENV GO111MODULE="off"
RUN CGO_ENABLED=0 go build -o /bin/demo

FROM scratch
COPY --from=build /bin/demo /bin/demo
EXPOSE 8888
CMD ["/bin/demo"]

Generiamo l'immagine myhello:

docker build -t myhello .

Aggiungiamo un tag all'immagine generata, che indichi il nostro repository locale sullo host:

docker tag myhello:latest localhost:5000/myhello:latest

Compiamo il push al repository locale sullo host. Grazie ai nostri host mapping, l'immagine diventa disponibile anche sul servizio registry dello swarm.

docker push localhost:5000/myhello:latest

Generiamo un servizio sullo swarm:

dk docker service create --name myhello \
  -p 8888:8888 \
  myregistry.com:5000/myhello

Listiamo i servizi dello swarm:

dk docker service ls

Testiamo il servizio dallo host:

curl manager:8888

Scaliamo il servizio sullo swarm:

dk docker service scale myhello=5

Proviamo più volte ad accedere al servizio dallo host:

curl manager:8888

Ogni volta il rapporto da uno hostname diverso.

Docker Stack

Il comando docker stack è simile al comando docker compose: compie il deployment di un numero di servizi orchestrati, ma come servizi swarm anzichè locali.

I servizi sono specificati in un file di configurazione YAML, che può essere benissimo docker-compose.yml. La sintassi è quella della versione 3.

Non è richiesta la specifica di versione nel file di configurazione: se cìè viene generato un warning ma il file è accettato comunque. La presenza di una specifica di versione permette di creare un file docker-compose.yml accettabile sia da docker compose che da docker stack.

E' buona norma di debugging testare il nostro applicativo con docker compose prima di sottoporlo a docker stack.

Vi sono comunque alcune limitazioni organizzative da considerare nella preparazione di un docker-compose.yml.

Prima demo

Preparare lo scaffolding:

mkdir -p ~/swarm/stackdemo
cd ~/swarm/stackdemo

Un applicativo scritto in Python:

vi app.py
from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! I have been seen {} times.\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=True)

File di requisiti:

vi requirements.txt
flask
redis

Costruzione del Dockerfile:

vi Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.4-alpine
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

File di specifiche compose/stack:

vi docker-compose.yml
  services:
    web:
      image: 127.0.0.1:5000/stackdemo
      build: .
      ports:
        - "8000:8000"
    redis:
      image: redis:alpine

Installiamo un registry locale sulla macchina host:

docker run -d -p 5000:5000 --restart=always --name registry -v ~/registry:/var/lib/registry registry

Inviamo l'immagine al registry locale della macchina host:

docker-compose push

Nella nostra organizzazione le immagini del registry locale sono mappate anche alla directory ~/registry.

Copiamo il file di specifiche alla directory ~/manager-tmp, che è mappata a /tmp del nodo manager:

cp docker-compose.yml ~/manager-tmp

Ora possiamo compiere il deploy dello stack su manager:

dk docker stack deploy --compose-file /tmp/docker-compose.yml stackdemo

Controlliamo l'avvenuto deploy con:

dk docker stack ls

e con

dk docker stack ps  stackdemo

Testiamo il funzionamento dal nodo manager del cluster:

dk curl http://localhost:8000

oppure da un altro nodo del cluster:

dk curl http://192.168.77.4:8000

oppure dalla macchina host

curl http://manager:8000

Rimozione dello stack:

dk docker stack rm stackdemo

Seconda demo

L'esercizio di postgres e pgadmin.

Prepparare lo scaffolding:

mkdir -p ~/swarm/pg
cd ~/swarm/pg

Preparare il docker-compose:

vi docker-compose.yml
version: '3'

services:
  postserver:
    image: myregistry.com:5000/postgres:12.2
    environment:
      POSTGRES_PASSWORD: "secret"
    ports:
      - "5432:5432"
    volumes:
      - /data:/var/lib/postgresql/data
    networks:
      postnet:


  pgadmin:
    image: myregistry.com:5000/dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: "mich@stormforce.ac"
      PGADMIN_DEFAULT_PASSWORD: "supersecret"
    ports:
      - "8000:80"
    depends_on:
      - postserver
    networks:
      postnet:

networks:
  postnet:
    name: postnet

Copiare il file nella directory condivisa:

cp docker-compose.yml ~/manager-tmp

Compiere il deployment dello stack:

dk docker stack deploy --compose-file /tmp/docker-compose.yml pg

Verificare connettendosi a postadmin.

Conclusione

Software Stack

Docker ha iniziato la sua vita nel 2013. Non era l'unica iniziativa che prevedeva l'importanza dei contenitori, ma era quella che più ha attirato l'attenzione degli utenti.

Lo scenario dell'universo dei contenitori, percepito dai più, era allora abbastanza semplice.

Dbefore

Il successo di Docker ha generato competizione, fino alla necessità di introdurre degli standard per equiparare il funzionamento di offerte diverse.

La Open Container Initiative (OCI) ha definito un'interfaccia standard dei contenitori con gli strati superiori dello stack. Docker si è adattata, e ha suddiviso il server di gestione contenitori in

  • runC - a basso livello, interfacciante il kernel Linux
  • containerd - a livello superiore ad OCI, fornente un'interfaccia standard

Simultaneamente offerte diverse, come Kubernetes, per sfruttare l'onda Docker, ma anche offerte concorrenti, hanno sviluppato un'interfaccia a livello più elevato, la Container Runtime Interface (CRI). E'stato sviluppato software adattatore cri-containerd sopra il containerd di Docker.

Come risultato, lo scenario dello stack software in cui si trova Docker è ora molto più complesso.

Dafter

E lo scenario è in continua evoluzione.

Orchestrazione

Docker fornisce uno strumento conosciuto e ben affermato per il packaging di applicativi in contenitori, secondo standard accettati.

Il vero requisito corrente è la realizzazione, deployment e amministrazione automatica di applicativi complessi, suddivisi in molti contenitori coordinati: il problema dell'orchestrazione.

Così come Docker Compose è un semplice strumento di orchestrazione di contenitori su una unica piattaforma host, desideriamo una orchestrazione remota e distribuita in un cluster di piattaforme in ambito globale.

Vi sono correntemente due grosse soluzioni:

  • Kubernetes - della Google
    • Favorito. Complesso e veramente globale. Implementato a volte in offerte proprietarie, come OpenShift della Red Hat.
  • Docker Swarm - della Docker.io
    • In calo temporaneo di stima, ma non veramente sfavorito. Con vantaggi architettonici e implementativi per chi già ha esperienza Docker.

Altre soluzioni esistono. Il recente diagramma mostra la situazione delle preferenze.

Platforms