Docker Swarm
Proseguiamo con la serie rivolta a Docker e oggi vi porterò un laboratorio che coinvolge Docker Swarm.
Docker Swarm è stata la prima risposta all’esigenza di scalabilità e alta affidabilità che chi utilizza docker ha avuto la possibilità di utilizzare. Con pochi e semplici comandi riusciamo a creare un cluster perfettamente funzionante in grado di gestire il carico di lavoro e offrire l’alta affidabilità.
Swarm, nasce dallo stesso brodo primordiale di Docker e permette di gestire un qualsiasi numero di host docker in un unico cluster fornendo una gestione centralizzata dei cluster e l’orchestrazione dei container.
Docker Swarm si basa su un’architettura master-slave, il che significa che ogni cluster ospiterà almeno un nodo manager e una serie di N nodi worker (gli Umpa Lumpa dello sciame). Il manager è responsabile dell’amministrazione del cluster e l’assegnazione dei compiti, i worker prendono in carico l’esecuzione dei task necessari alle varie attività. Inoltre le applicazioni vengono suddivise in services che a loro volta possono essere suddivisi un più container.
Nella terminologia di Docker il concetto di “servizio” indica una struttura astratta mediante la quale potete definire compiti che devono essere eseguiti nel cluster. Ogni servizio è suddiviso in task singoli. Alla creazione di un servizio determinerete su quale immagine docker si basarà e quindi quali comandi verranno eseguiti nel container. Docker Swarm permette di definire i servizi in due modalità: servizi replicati e globali.
Servizi replicati: un servizio replicato è un task che viene eseguito in un numero di repliche predeterminate. Ogni replica è un’istanza del container definito nel servizio.
Servizi globali: eseguendo un servizio come globale, ogni nodo disponibile nel cluster avrà un task per il relativo servizio. Se aggiungete un nuovo nodo al cluster, lo swarm manager gli assegnerà immediatamente un task.
Un vantaggio tangibile di Swarm è la distribuzione dei task e quindi del carico in modo automatico.
Visione d’insieme
Nell’immagine si vede la situazione finale ovvero N nodi ( macchine virtuali o fisiche ), un volume GlusterFS condiviso tra tutti i nodi e il docker engine. GlusterFS e docker sono servizi installati direttamente sui modi mentre Nginx e Portainer sono dei docker service (container). Di seguito vedremo i passi di preparazione degli hosts, creazione del volume GlusterFS e del cluster Docker Swarm.
Preparazione degli hosts
In questo scenario lavoreremo con 3 nodi; Come prima cosa predisponiamo i server Linux che parteciperanno al cluster. In questo scenario utilizzerò delle istanze compatibili RHEL 8 (Rocky Linux, Alma Linux, ecc)
NB: queste operazioni devono essere effettuate su tutte le istanze
Ogni server avrà un disco di root “/” più un disco per lo storage.
# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sr0 11:0 1 4M 0 rom
vda 252:0 0 10G 0 disk
├─vda1 252:1 0 1M 0 part
└─vda2 252:2 0 10G 0 part /
vdb 252:16 0 100G 0 disk
Creiamo la partizione 1 e formattiamola in xfs
# fdisk /dev/vdb
Welcome to fdisk (util-linux 2.32.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.
Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0x9ec037e0.
Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)
Select (default p):
Using default response p.
Partition number (1-4, default 1):
First sector (2048-209715199, default 2048):
Last sector, +sectors or +size{K,M,G,T,P} (2048-209715199, default 209715199):
Created a new partition 1 of type 'Linux' and of size 100 GiB.
Command (m for help): p
Disk /dev/vdb: 100 GiB, 107374182400 bytes, 209715200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x9ec037e0
Device Boot Start End Sectors Size Id Type
/dev/vdb1 2048 209715199 209713152 100G 83 Linux
Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
# mkfs.xfs /dev/vdb1
meta-data=/dev/vdb1 isize=512 agcount=4, agsize=6553536 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=0
= reflink=1 bigtime=0 inobtcount=0
data = bsize=4096 blocks=26214144, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0, ftype=1
log =internal log bsize=4096 blocks=12799, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
Discarding blocks...Done.
cerchiamo l’id univoco della partizione e sistemiamo l’fstab
# mkdir /brick01
# ls -l /dev/disk/by-uuid/
total 0
lrwxrwxrwx 1 root root 9 Jul 23 08:42 2022-07-23-10-30-11-00 -> ../../sr0
lrwxrwxrwx 1 root root 10 Jul 23 09:01 265dfba8-24e1-4b40-a748-e802f8f1fe2d -> ../../vdb1
lrwxrwxrwx 1 root root 10 Jul 23 08:42 fe3a1123-51c6-4a49-a499-857cc079d6a9 -> ../../vda2
editiamo il file /etc/fstab e aggiungiamo la riga per la partizione sdb1
UUID=265dfba8-24e1-4b40-a748-e802f8f1fe2d /brick01 xfs defaults 0 0
verifichiamo che sia tutto ok
# mount -a
Nel caso in cui non avessimo un server dns locale possiamo configurare il file hosts per la risoluzione dei nomi.
editiamo il file /etc/cloud/templates/hosts.redhat.tmpl
## template:jinja
{#
This file /etc/cloud/templates/hosts.redhat.tmpl is only utilized
if enabled in cloud-config. Specifically, in order to enable it
you need to add the following to config:
manage_etc_hosts: True
-#}
# Your system has configured 'manage_etc_hosts' as True.
# As a result, if you wish for changes to this file to persist
# then you will need to either
# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl
# b.) change or remove the value of 'manage_etc_hosts' in
# /etc/cloud/cloud.cfg or cloud-config from user-data
#
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 {{fqdn}} {{hostname}}
127.0.0.1 localhost.localdomain localhost
127.0.0.1 localhost4.localdomain4 localhost4
# The following lines are desirable for IPv6 capable hosts
::1 {{fqdn}} {{hostname}}
::1 localhost.localdomain localhost
::1 localhost6.localdomain6 localhost6
192.168.1.42 maurizio
192.168.1.41 gigi
192.168.1.43 nina
Se non utilizzi cloud-template puoi modificare direttamente /etc/hosts.
Pacchetti e moduli
Installiamo docker
# dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
# dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# systemctl enable --now docker
Dopo aver installato e avviato docker aggiungiamo anche i kernel-headers e carichiamo dei moduli all’avvio.
# dnf install -y kernel-headers
# echo 'modprobe ip_tables' >> /etc/modules && echo 'modprobe ip_vs' >> /etc/modules
# modprobe ip_tables
# modprobe ip_vs
GlusterFS
Per installare GlusterFS avremmo bisogno del repository. Installiamo il pacchetto che contiene i repos
# dnf install -y centos-release-gluster9
editiamo il file /etc/yum.repos.d/CentOS-Gluster-9.repo e aggiungiamo la baseurl
baseurl=https://dl.rockylinux.org/vault/centos/8.5.2111/storage/x86_64/gluster-9/
installiamo i pacchetti
# dnf install -y glusterfs glusterfs-libs glusterfs-server
avviamo e abilitiamo i servizi
# systemctl enable --now glusterfsd.service glusterd.service
aggiungiamo i nodi al cluster
# gluster peer probe nina
# gluster peer probe maurizio
...
creiamo e avviamo il volume replicato
# gluster volume create docker replica 3 maurizio:/brick01 gigi:/brick01 nina:/brick01 force
# gluster volume start docker
NB: il force serve perché non ho voluto creare un ulteriore sottocartella in /brick01.
Nel mio caso ho optato per 3 nodi con 1 brick per nodo
Ora creiamo il mount per i volumi docker
# mkdir /opt/docker
creiamo il file /etc/systemd/system/opt-docker.mount
[Unit]
Description=GlusterFS Mount
After=glusterfsd.service
Requires=glusterfsd.service
[Mount]
What=localhost:/docker
Where=/opt/docker
Type=glusterfs
Options=defaults,_netdev
[Install]
WantedBy=multi-user.target
avviamo e abilitiamo il mount
# systemctl enable --now opt-docker.mount
A questo punto avremo a disposizione /opt/docker come storage ridondato e sincronizzato per i nostri volumi docker.
Docker Swarm
Inizializziamo il cluster
# docker swarm init
verranno stampati i comandi da lanciare sui nodi master e i nodi worker. Copiamo quello che ci serve.
per un cluster di test va bene anche un solo nodo eleggibile come master ma per cluster di produzione ne consiglio almeno 3
su ogni altro nodo lanciamo
# docker swarm join .....
su uno dei nodi e verifichiamo che ci siano tutto dal nodo “leader”
# docker node ls
KeepAlived
Per gestire meglio l’alta affidabilità opteremo per un vecchio e affidabile amico: KeepAlived. Non installeremo KeepAlived direttamente sui nodi ma utilizzeremo un container.
Sul nodo “master”
# docker run -d --name keepalived --restart=always \
--cap-add=NET_ADMIN --cap-add=NET_BROADCAST --cap-add=NET_RAW --net=host \
-e KEEPALIVED_UNICAST_PEERS="#PYTHON2BASH:['192.168.1.41', '192.168.1.42', '192.168.1.43']" \
-e KEEPALIVED_VIRTUAL_IPS=192.168.1.40/24 \
-e KEEPALIVED_PRIORITY=200 \
-e KEEPALIVED_INTERFACE="eth0" \
osixia/keepalived:2.0.20
sui nodi “backup”
# docker run -d --name keepalived --restart=always \
--cap-add=NET_ADMIN --cap-add=NET_BROADCAST --cap-add=NET_RAW --net=host \
-e KEEPALIVED_UNICAST_PEERS="#PYTHON2BASH:['192.168.1.41', '192.168.1.42', '192.168.1.43']" \
-e KEEPALIVED_VIRTUAL_IPS=192.168.1.40/24 \
-e KEEPALIVED_PRIORITY=100 \
-e KEEPALIVED_INTERFACE="eth0" \
osixia/keepalived:2.0.20
Il parametro “-e KEEPALIVED_INTERFACE” è molto importante perché dichiara su quale interfaccia keepalived comunicherà con gli altri nodi e annuncerà il virtual IP.
Possiamo quindi avere un volume per il nostro primo servizio nginx distribuito.
# mkrir -p /opt/docker/nginx/html && echo "NGINX SWARM" > /opt/docker/nginx/html/index.html
# docker service create \
--mode global \
--publish mode=host,target=80,published=9999 \
--mount type=bind,src=/opt/docker/nginx/html,dst=/usr/share/nginx/html \
--name=nginx_replicated \
nginx:latest
..........
overall progress: 4 out of 4 tasks
............: running [==================================================>]
............: running [==================================================>]
............: running [==================================================>]
............: running [==================================================>]
verify: Service converged
al termine avremo il nostro nginx servito sulla porta 9999 del nostro cluster.
# curl http://[ip-host]:9999
per rimuovere il service nginx
# docker service rm nginx_repliceted
nginx_replicated
Mani in pasta
Web Gui
Come gui ho optato per Portainer : per installarlo sul nostro cluster prepariamo il file stack, creiamo la cartella /opt/docker/portainer e il file portainer-agent-stack.yml
version: '3.2'
services:
agent:
image: portainer/agent:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- agent_network
deploy:
mode: global
placement:
constraints: [node.platform.os == linux]
portainer:
image: portainer/portainer-ce:latest
command: -H tcp://tasks.agent:9001 --tlsskipverify
ports:
- "9443:9443"
- "9000:9000"
- "8000:8000"
volumes:
- /opt/docker/portainer/portainer_data:/data
networks:
- agent_network
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
networks:
agent_network:
driver: overlay
attachable: true
e lanciamo
# mkdir portainer_data
# docker stack deploy -c portainer-agent-stack.yml portainer
ora possiamo fare login http://[ip-nodo-swarm]:9443
Proxy
Anche in questo caso ci affidiamo a Nginx Proxy Manager .
Creiamo la cartella /opt/docker/nginx-pm e le sottocartelle data, data/mysql e letsencrypt.
# mkdir -p /opt/docker/nginx-pm/data/mysql /opt/docker/nginx-pm/letsencrypt
Il file docker-stack.yml che possiamo usare per creare lo stack dalla gui di portainer
version: "3"
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
ports:
# Public HTTP Port:
- '80:80'
# Public HTTPS Port:
- '443:443'
# Admin Web Port:
- '81:81'
environment:
# These are the settings to access your db
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
# If you would rather use Sqlite uncomment this
# and remove all DB_MYSQL_* lines above
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- /opt/docker/nginx-pm/data:/data
- /opt/docker/nginx-pm/letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: 'jc21/mariadb-aria:latest'
environment:
MYSQL_ROOT_PASSWORD: 'PASSWORD'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
volumes:
- /opt/docker/nginx-pm/data/mysql:/var/lib/mysql
Mi raccomando le password!
inseriamo il file e deployamo lo stack.
Crowdsec
Sui nostri server installiamo crowdsec in modalità centralizzata così da avere i balancer su tutti i nodi ma l’api su un nodo solo. Puoi seguire questa guida
Se va tutto per il meglio, una volta aggiunta l’istanza all’app cloud https://app.crowdsec.net/ il risultato sarà simile a questo.
Extra
Aggiungere un nuovo nodo
GlusterFS
Se volessimo aggiungere un nuovo nodo dopo aver installato GlusterFS, lanciamo il comando
# gluster volume add-brick docker replica 4 kate:/brick01 force
In questo modo aggiungiamo un nuovo brick e abilitiamo incrementiamo la replica così da avere i dati su tutti i nodi.
Swarm
Dobbiamo ottenere il token
# docker swarm join-token worker
e lanciare il comando join dal nuovo nodo.
# docker swarm join .....
KeepAlived
Fermiamo e creiamo i nuovi container con il nuovo nodo
# docker stop keepalived && docker rm keepalived
# docker run -d --name keepalived --restart=always \
--cap-add=NET_ADMIN --cap-add=NET_BROADCAST --cap-add=NET_RAW --net=host \
-e KEEPALIVED_UNICAST_PEERS="#PYTHON2BASH:['192.168.1.41', '192.168.1.42', '192.168.1.43', '192.168.1.44']" \
-e KEEPALIVED_VIRTUAL_IPS=192.168.1.40 \
-e KEEPALIVED_PRIORITY=100 \
osixia/keepalived:2.0.20
Le opinioni in quanto tali sono opinabili e nulla ti vieta di approfondire l’argomento.
Risorse: