Marvin Pascale

[B.Log]

30 Luglio 2022

Docker Swarm

Proseguiamo con la serie rivolta a Docker e oggi vi porterò un laboratorio che coinvolge Docker Swarm. Docker

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

Docker 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!

Stack

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.

Stack

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: