Introduzione
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
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
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
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
Contenitori: Virtualizzazione dell'Ambiente Operativo
- Più efficienza
- Meno manutenzione di sistema
- Partenza molto più veloce
- Migliaia di immagini disponibili, con documentazione
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
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.
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
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
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
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.
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.
- Soluzione per principianti
- Interfaccia grafica Dashboard comune
- Molte meno opzioni che da linea di comando
- Non ancora performante o stabile
- Promesse di migliorie
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
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
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
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
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
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
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
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
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
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
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
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
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 parametroEXPOSE 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.
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
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.
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 LinuxRUN ["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 execCMD comando param1 param2
- formato shellCMD ["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 builddestinazione
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
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
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 "$@"
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:
- creare un Dockerfile e lanciare un docker build
- 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:
- Non è una buona idea creare immagini contenenti applicativi grafici interattivi
- 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
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
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.
Un applicativo può essere complesso e avere più containers intercollegati:
- Sequenza di creazione?
- Interconnessioni?
- Dipendenze?
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.
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.
Profilo di terminale
Nuovo profilo: one.
Comando di lancio:
bash -c "docker exec -ti one sh -c \"export PS1='\h:\W\# ' && sh"\"
Per renderlo distintivo, cambiamo i colori del terminale.
Nuova finestra di terminale e collegamento con SSH.
Profilo per two
Ripetere per un nuovo profilo two.
Schema di colori: Green on black.
Esercizio 2: comunicazione con host
02net1-ccli-chttp
Una rete, client e server HTTP.
Collegamento a server HTTP dal browser dello host.
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
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.
Esercizio 2A
Una rete, client e server HTTP.
Da tcpdump
di un container a Wireshark
sullo host.
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.
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.
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.
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
- Default:
POSTGRES_PASSWORD
- Necessario semprePOSTGRES_DB
- DB a cui connettersi. Default:$POSTGRES_USER
POSTGRES_INITDB_ARGS
- Se la datadir è inizializzata, argomenti e opzioni per initdbPOSTGRES_INITDB_WALDIR
- Locazione del Write Ahead Log se diversa dalla datadirPOSTGRES_HOST_AUTH_METHOD
- Popolapg_hda.conf
.- Default "
host all all all md5
"
- Default "
PGDATA
- Locazione della datadir.- Default:
/var/lib/postgresql/data
- Default:
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.
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.
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
-
Installazione di Ubuntu Server
-
Configurazione di Port Forwarding di VirtualBox
-
Login senza password
-
sudo senza password
-
Collegamento da Gnome Terminal
-
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
- 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.
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
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.
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 certificatoregistry.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 condocker node ls
node.hostname
- listato condocker 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
.
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 esseremanager
. - 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.
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.
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.