Vue normale

Reçu avant avant-hierWelcome to /dev/null

Déployer un service hautement disponible grâce à Redis Replication (Publié dans Linux Pratique n°140)

31 décembre 2024 à 13:37

Redis est un système de stockage de données en mémoire de type NoSQL orienté hautes performances. Il est souvent utilisée comme base de données, système de cache ou de messages. Nous verrons dans cet article comment déployer un service hautement disponible grâce à Redis Replication.

1 Notre premier serveur Redis

Pour démarrer, commençons avec un mon serveur Redis. L’installation sera présentée pour Ubuntu 22.04 et devrait fonctionner sans trop d’adaptations pour Debian ou RHEL-like.

1.1 Installation

Redis est disponible dans les dépôts classiques de la distribution. Une configuration minimale peut être faite en installant simplement les paquets et en activant le service systemd.

sudo apt -y install redis-server
sudo systemctl enable --now redis-server

Vérifions maintenant que nous parvenons à nous connecter :

redis-cli
127.0.0.1:6379> ping
PONG

La CLI permet également de récupérer avec la commande INFO un certain nombre d’éléments sur le fonctionnement du serveur. Celle-ci s’appelle seule ou avec un argument pour obtenir les informations relatives à une catégorie particulière.

127.0.0.1:6379> INFO Server
# Server
redis_version:6.0.16
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:a3fdef44459b3ad6
redis_mode:standalone
os:Linux 5.15.0-73-generic x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:11.2.0
process_id:596
run_id:7ea0cf9f46b211a64874d7a1c0a115be78c42e98
tcp_port:6379
uptime_in_seconds:61483
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:8771018
executable:/usr/bin/redis-server
config_file:/etc/redis/redis.conf
io_threads_active:0

Redis gère plusieurs types de données. Il est possible d’utiliser des chaînes de caractères, des ensembles, des hashs, des listes ainsi que d’autres types de données. Pour procéder à une première vérification de bon fonctionnement, nous allons ainsi écrire une donnée de type string, récupérer la valeur en indiquant la clé puis effacer cette donnée.

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> get foo
"bar"
127.0.0.1:6379> del foo
(integer) 1
127.0.0.1:6379> get foo
(nil)

A ce stade, notre serveur est fonctionnel, toutefois sa configuration est très basique. Redis n’écoute que sur l’interface localhost ce qui est incompatible avec la notion de réplication que nous mettrons en place et il est préférable que systemd prenne en charge le service de manière explicite. Nous allons entreprendre donc nos premières modifications du fichier de configuration, /etc/redis/redis.conf et remplacer les paramètres bind et supervised par les valeurs suivantes :

bind 0.0.0.0
supervised systemd
protected-mode

Redémarrons le service redis et vérifions :

sudo systemctl restart redis-server
sudo netstat -lataupen |grep ":6379"
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      115        39165      3267/redis-server 0

En pratique, la plupart des options de Redis peuvent être définies avec la commande CONFIG SET. C’est peut-être moins classique ou confortable qu’un fichier de configuration mais c’est ce qui permet à Redis d’être entièrement reparamétré sans qu’il ne soit nécessaire de relancer le service et donc sans aucune interruption de service.

1.2 Gérer la persistance

Redis est une base de données en mémoire. Par conséquent si le service est arrêté, quelle qu’en soit la raison, les données sont perdues. Pour assurer la persistance des données, Redis permet d’utiliser deux mécanismes. Dans le premier, appelé RDB, Redis va générer des snapshots à intervalles réguliers de la base de données. Cela se configure avec save suivi de deux indicateurs, le premier consiste en une sauvegarde après n secondes si au moins un certain nombre d’écritures ont eu lieu. Ce paramètre est en outre multivalué .La politique par défaut est celle-ci :

save 900 1
save 300 10
save 60 10000

Généralement, on souhaite avoir au moins un snapshot récent même s’il y a peu d’écritures, d’où celui réalisé après 15 minutes à partir du moment où il y a eu une écriture. A l’inverse, en cas de forte charge, on souhaite également avoir des snapshots fréquents pour minimiser la perte de données, d’où le snapshot toutes les minutes dès 10000 clés modifiées. Ces seuils sont naturellement à adapter en fonction de l’activité de la base et afin de minimiser la perte de données admissible.

Dans le second mécanisme, AOF pour Append Only File, la persistance est gérée via l’utilisation d’un fichier de logs qui va historiser chaque écriture. Pour un maximum de fiabilité, les deux modes peuvent être utilisés conjointement. Naturellement, cela a un impact sur la consommation mémoire et les I/O disques car pour chaque écriture en base, une seconde est faite pour le journal AOB.

Pour utiliser l’AOF, ajoutons ces deux lignes dans le fichier redis.conf :

appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

1.3 Un minimum de sécurité

Par défaut, Redis n’est pas joignable via le réseau. Pour que la mise en œuvre de la réplication à suivre soit possible, le service écoute désormais sur l’ensemble des interfaces réseau. Bien qu’il soit possible de restreindre à une ou plusieurs interfaces, l’accès au service à des fins de test s’est réalisé sans authentification. En définissant un mot de passe sur le paramètre requirepass, toute opération sur Redis nécessitera au préalable de s’authentifier avec la commande AUTH suivie du mot de passe. Dans la configuration redis.conf nous ajoutons donc :

requirepass SECRET

Et nous pouvons vérifier :

# redis-cli
127.0.0.1:6379> AUTH SECRET
OK

Pour assurer une meilleur sécurité, du TLS devrait être mis en œuvre et les ACL Redis 6 devraient être utilisées.

2 Assurer la haute disponibilité

2.1 Les mécanismes

Redis Replication est un système maître/esclave permettant de répliquer de manière asynchrone les données du master sur les slaves. En cas de perte de connectivité avec le master, les slaves vont tenter de resynchroniser l’écart de données dès que la connexion redevient disponible. Si cela n’est pas possible une resynchronisation complète est réalisée. Un slave peut servir de source de réplication à un autre slave et les répliquas peuvent servir de serveur en lecture pour répartir la charge et donc augmenter la performance.

En cas de perte du master, une procédure de failover doit être mise en œuvre pour promouvoir un slave comme master. Tant que cela n’est pas fait, le fonctionnement est en mode dégradé si ce n’est totalement interrompu. Redis Sentinel utilisé en complément permet de superviser le fonctionnement de Redis et en cas de panne du master, de promouvoir un nouveau master et de reconfigurer les repliquas pour qu’ils se répliquent depuis ce nouveau master. Dans ce type d’architecture, il est recommandé de disposer d’au moins 3 serveurs pour gérer le quorum.

Avec Redis Replication il est fortement recommandé d’activer la persistance. En effet, sans persistance et en cas de redémarrage du master, il se retrouvera vide de toute donnée. Dans un second temps, les slaves vont s’empresser de répliquer cela les vidant de leurs données.

Pour assurer la montée en charge horizontale, Redis Cluster offre des possibilités complémentaires. Dans ce mode, il s’agit toujours d’avoir une architecture master/slave mais avec gestion et failover automatique. L’espace de clés est divisé de sorte que chaque groupe de serveur ne gère qu’une partition de la base de données, cela s’appelle du sharding. La taille de la base de données peut ainsi être plus importante et la charge peut être répartie sur plus de nœuds. Un minimum de 6 nœuds est toutefois requis pour Redis Cluster ce qui prédispose ce mode à des infrastructures relativement conséquentes.
De fait dans cet article j’aborderai uniquement Redis Replication.

2.2 Configuration

La configuration mise en œuvre sur le premier serveur que nous appellerons db01 devra être reprise à l’identique sur nos deux slaves. Le serveur DB01 sera le master par défaut. Pour que la suite soit aisément compréhensible, voici les serveurs et adresses IP que je vais utiliser :

  • db01 : 192.168.69.81
  • db02 : 192.168.69.82
  • db03 : 192.168.69.83

Sur chacun de nos serveurs, nous allons définir la valeur de masterauth. En effet, nous avons exigé précédemment un mot de passe via requirepass, il faut donc s’authentifier pour avoir les droits pour répliquer :

masterauth SECRET

Et enfin, sur chaque slave, on indique depuis quel serveur notre slave doit se répliquer :

replicaof db01.morot.local 6379

Il suffit de redémarrer nos serveurs, il n’y a rien de plus à faire.

2.3 Vérification de la réplication

Dans un premier temps, envoyons la commande INFO Replication sur notre master. Si la configuration est correcte, alors on doit pouvoir avoir un rôle master, visualiser les deux slaves comme connectés ainsi que leurs adresses IP et enfin en fonctionnement nominal un statut online :

127.0.0.1:6379> AUTH SECRET
OK
127.0.0.1:6379> INFO Replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.69.83,port=6379,state=online,offset=1106,lag=0
slave1:ip=192.168.69.82,port=6379,state=online,offset=1106,lag=0
master_replid:75b55df27cafe6bfd7aec3114bfd63e77d45a8cb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1106
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1106

A l’inverse, sur un slave, le rôle doit correspondre et le serveur doit être connecté au slave (master_link_status). Si master_sync_in_progress est à 0 alors la réplication est terminée. La section master_replid indiqué le dataset de ReplicationID qui doit être identique avec le master lorsque les masters et slaves sont synchronisés. Si une synchronisation était en cours, nous aurions des champs master_sync supplémentaires pour suivre la volumétrie à répliquer et les performances de réplication.

127.0.0.1:6379> INFO Replication
# Replication
role:slave
master_host:db01.morot.local
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_repl_offset:1288
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:75b55df27cafe6bfd7aec3114bfd63e77d45a8cb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1288
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1288

2.4 Failover

Voyons désormais ce qu’il se passe lors d’une panne du master. J’ai simulé un crash du master en coupant violemment la VM qui porte le service. Sur chacun de mes slaves, la réplication indique que le lien avec le master est « down » avec le délai depuis lequel le lien est rompu.

127.0.0.1:6379> INFO Replication
# Replication
role:slave
master_host:db01.morot.local
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:3472
master_link_down_since_seconds:42

S’il s’agit d’un simple redémarrage du serveur master, la réplication devrait remonter dès que le serveur sera up. En cas de panne majeure, le service rendu est interrompu. Il faut manuellement reconfigurer les slaves pour rétablir le service.

Sur db03, nous allons donc indiquer que sa source de réplication est db02 :

127.0.0.1:6379> replicaof db02.morot.local 6379
OK

Pour autant, cela ne fait pas de db02 un master par magie. D’autant que Redis supporte la réplication d’un slave depuis un autre slave. Il faut donc indiquer que db02 est un master ce qui se configure en indiquant qu’il est le répliqua d’aucun serveur :

127.0.0.1:6379> replicaof no one
OK
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.69.83,port=6379,state=online,offset=3472,lag=0

2.5 Le retour du master

Se pose ensuite la question de la remise en service du serveur db01 lorsqu’il sera réparé. Avec le failover précédent il va se retrouver master sans slave et il sera aussi désynchronisé des autres serveurs Redis. Il est donc indispensable de le remettre en synchronisation avec le master qui a été promu. Cela se fait de la même façon que lors du failover précédent :

127.0.0.1:6379> replicaof db02.morot.local 6379
OK

Et c’est tout. Si toutefois vous tenez à remettre le serveur redis master « nominal » en tant que master, il faut alors :

  • Le resynchroniser sur le master et s’assurer que la réplication est terminée
  • Définir les slaves en état nominal (db02 et db03) comme replicaof db01
  • Indiquer que db01 n’est plus le répliqua d’aucun serveur.

Cela reste toutefois une opération à réaliser en période de maintenance car une courte rupture du service lors de la bascule est à prévoir.

3 Failover automatique avec Sentinel

3.1 Configuration

Nous venons de le voir, la réplication est simple à mettre en œuvre ou à reconfigurer. Elle a toutefois un énorme inconvénient en cas de perte du master, elle est manuelle. Nous allons donc mettre en œuvre un système complémentaire appelé Redis Sentinel qui viendra gérer automatiquement la promotion d’un nouveau master et la reconfiguration des slaves.
Commençons par installer sentinel :

sudo apt -y install redis-sentinel

Et configurer le service pour qu’il communique sur le réseau et non plus uniquement sur localhost. Au niveau firewall, le port 26379 devra autoriser les flux. Cela se fait via le fichier dédié /etc/redis/sentinel.conf :

bind 0.0.0.0
port 26379

Ajoutons un mot de passe de connexion, il s’agit du mot de passe d’authentification sur Sentinel :

requirepass SECRET

Enfin démarrons la configuration des règles de bascule. Nous monitorons l’adresse IP du master, 192.168.69. Un quorum de 2 serveurs est exigé, c’est à dire que si au moins deux slaves s’accordent sur le fait que le master est injoignable alors une bascule est enclenchée.

sentinel monitor mymaster 192.168.69.81 6379 2
sentinel auth-pass mymaster SECRET

Le master est considéré comme injoignable après une minute :

sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

Une redémarrage du service sentinel plus tard, vérifions l’état de notre service en se connectant via redis-cli sur le port de Sentinel cette fois :

redis-cli -p 26379
127.0.0.1:26379> AUTH SECRET
OK
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.69.81:6379,slaves=2,sentinels=4

Nous retrouvons bien le bon nombre de slaves. Si on souhaite des informations détaillées sur les slaves il est possible d’envoyer la commande sentinel slaves mymaster et pour les instances sentinel : sentinel sentinels mymaster. La sortie étant peu lisible, il s’agit de la configuration en mode clé valeur dans la base Redis, je ne vais pas l’inclure ici.

3.2 Test de bascule

Sur notre master, simulons une panne en arrêtant le service avec systemctl stop redis-server. En parallèle, regardons les logs de Sentinel dans /var/log/redis/redis-sentinel.log. Par lisibilité, je vais filtrer quelques peu les logs.

En premier lieu nous avons un event sdown, cela signifie que sentinel a détecté que le master n’était plus joignable :

37873:X 18 Jun 2023 21:33:04.343 # +sdown master mymaster 192.168.69.81 6379

Ensuite, nous avons un odown, c’est à dire que le serveur est vu comme injoignable par au moins le nombre de serveur du quorum, comme nous avons indiqué 2, il faut au moins les deux serveurs pour qu’une promotion d’un nouveau soit réalisée :

37873:X 18 Jun 2023 21:33:05.420 # +odown master mymaster 192.168.69.81 6379 #quorum 3/2

db03 a été élu comme nouveau master :

37873:X 18 Jun 2023 21:34:17.255 # +selected-slave slave 192.168.69.83:6379 192.168.69.83 6379 @ mymaster 192.168.69.81 6379
37873:X 18 Jun 2023 21:34:17.437 # +promoted-slave slave 192.168.69.83:6379 192.168.69.83 6379 @ mymaster 192.168.69.81 6379

db02 est reconfiguré comme slave de db03 :

37873:X 18 Jun 2023 21:34:17.453 * +slave-reconf-sent slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.81 6379
37873:X 18 Jun 2023 21:34:17.510 * +slave-reconf-inprog slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.81 6379
37873:X 18 Jun 2023 21:34:18.623 * +slave slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.83 6379

Enfin, on peut le confirmer via la cli redis sur db03 :

127.0.0.1:6379> INFO REPLICATION
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.69.82,port=6379,state=online,offset=481996,lag=1
Félicitations, Redis s’est automatiquement reconfiguré sans intervention.
Que se passe-t-il désormais pour notre ancien master ? Au redémarrage de redis, il est devenu un slave. Cela se voit d’ailleurs dans les logs sentinel des autres serveurs. Il est possible de forcer via la cli sentinel un failover vers un autre serveur :
127.0.0.1:26379> SENTINEL failover mymaster
OK
127.0.0.1:26379> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.69.82:6379,slaves=2,sentinels=4

Il n’est toutefois pas possible d’indiquer expressément quel serveur sera promu. Une manière de le contrôler est d’utiliser la directive slave-priority pour favoriser un serveur avec une priorité plus faible.

4 HAProxy

Dans un monde idéal, votre client Redis aura un support de Sentinel. Il saura alors contacter sentinel pour connaître le master et s’y connecter. En récupérant la liste des serveurs, il sera en mesure de distribuer les requêtes en lecture sur les slaves pour répartir la charge. En cas d’anomalie du master, il saura enfin gérer le failover.

En pratique, on ne peut pas toujours connaître le niveau de support des clients notamment lorsque l’on fournit la plateforme à des développeurs tiers. Dans ce cas précis, je recommande d’utiliser le classique HAProxy qui sait tout faire en matière de répartition de charge que je vous laisse installer via les packages de la distribution.

Le cas particulier qu’il faut néanmoins gérer c’est la détection du master. De manière simple, si le port de destination répond, on considère souvent que le service est fourni par le backend. Dans notre cas, il va falloir identifier le serveur dont le rôle est le master, souvenez-vous nous avions l’information avec INFO Replication avec la CLI Redis. Il faut donc définir un TCP Check qui réalisera une authentification, enverra cette même commande est aura comme backend active celui dont le rôle est master :

frontend ft_redis
bind 0.0.0.0:6379 name redis
default_backend bk_redis

backend bk_redis
option tcp-check
tcp-check send AUTH\ SECRET\r\n
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send INFO\ Replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server r1 db01:6379 check inter 10s
server r2 db02:6379 check inter 10s
server r3 db03:6379 check inter 10s

Conclusion

C’est en terminé pour ce tour d’horizon de la haute disponibilité d’un service Redis Replication avec Sentinel. J’invite le lecteur curieux à compléter ce parcours pour utiliser les ACL et ajouter ce niveau de sécurité. J’ai volontairement écarté ce point pour légèrement simplifier la configuration et rester ciblé sur le sujet.

Galera, la solution pour des bases de données hautement disponibles (Publié dans Linux Pratique 137)

31 décembre 2024 à 11:27

Lorsqu’une application ne peut tolérer d’indisponibilités ou quand il devient nécessaire de fournir de la redondance au service de bases de données, il convient de mettre en place une architecture limitant les risques d’interruption de service. De même le besoin de performance peut se faire sentir avec la croissance du nombre d’utilisateurs. Galera est une extension pour MariaDB aidant à résoudre ces deux situations.

Galera est une extension pour Mariadb, MySQL et Percona XtraDB offrant des fonctionnalités de clustering multi-master. La réplication classique fonctionne en mode maitre-esclave, nécessitant donc en cas de panne ou de maintenance sur le master de mettre en œuvre un mécanisme de failover sur le slave pour le promouvoir. Cette opération est potentiellement complexe et manuelle.

Avec Galera il est possible de disposer de plusieurs serveurs master et donc disponibles en lecture comme en écriture. La réplication est opérée de manière synchrone et peut même être géo-répliquée.

Pour améliorer les performances, il devient possible d’augmenter le nombre de nœuds au sein du cluster, éventuellement complété d’une mise à l’échelle verticale, c’est à dire par ajout de vCPU et de RAM.

La haute disponibilité du service de bases de données devient donc native et ce, sans nécessiter de mettre en œuvre une opération de promotion du slave comme master. Le service est rendu tant qu’il y a au moins un nœud fonctionnel, toute question de capacité à tenir la charge mise à part.

Ensuite, Galera connaît très peu de limitations à la mise en cluster mais deux en particulier. La première a une portée en principe limitée, elle impose le moteur de stockage InnoDB, le moteur historique MyISAM est donc à oublier. La seconde, peut-être plus contraignante, est qu’une table doit avoir obligatoirement au moins une clé primaire. Cela peut donc demander du travail avec les développeurs qui vont coder avec cette base de données. Enfin, dernière limitation majeure mais ce n’en est pas une pour moi, Galera n’est disponible que sous Linux mais pas les autres systèmes supportés par MariaDB comme Windows.

Création du cluster

Un cluster Galera doit être composé d’un nombre impair de nœuds. Il n’est techniquement pas interdit de créer un cluster à seulement deux nœuds, ce n’est toutefois pas recommandé. En effet, on risque dans ce cas une situation de split brain. Un split brain est une situation où les nœuds qui composent le cluster se retrouvent isolés, par exemple en cas de coupure du réseau. Avec seulement deux nœuds, ils ne pourraient déterminer quel nœud se retrouve isolé. Cela impose donc au minimum de disposer de trois serveurs, sachant que l’ajout de nœuds au cluster est très aisé.
Pour la suite de cet article, je disposerai de quatre serveurs sous Ubuntu 22.04, trois serveurs de bases de données et un load balancer utilisé dans la suite de cet article :

  • db01 : 192.168.69.81
  • db02 : 192.168.69.82
  • db03 : 192.168.69.83
  • lb : 192.168.69.70

Installation de Mariadb

Dans un premier temps et sur chaque serveur composant le cluster, nous allons simplement y installer le service mariadb.

root@db01:~# apt -y install mariadb-server
root@db01:~# systemctl enable mariadb

Par défaut, MariaDB sous Ubuntu Server n’écoute que sur l’interface loopback. Comme les nœuds du cluster vont être accessibles et communiquer via le réseau, il faut faire en sorte que MariaDB écoute sur une socket accessible depuis les autres nœuds, idéalement en restreignant depuis une interface. Pour faire simple, nous allons le faire écouter sur toutes les interfaces :

root@db01:~# sed -i 's/^bind-address.*/bind-address = 0.0.0.0/g' /etc/mysql/mariadb.conf.d/50-server.cnf
root@db01:~# systemctl restart mariadb

Ajout du module Galera
Toujours sur chaque nœud, nous allons ensuite installer le module galera et éteindre mariadb

root@db01:~# apt -y install galera-4
root@db01:~# systemctl stop mariadb

Il faut maintenant activer la réplication avec le provider wsrep et surtout déclarer l’ensemble des nœuds participants au cluster. La configuration se fait dans le fichier /etc/mysql/mariadb.conf.d/60-galera.cnf :
wsrep_on active tout simplement la réplication Galera. Le nom du cluster doit quant à lui être identique sur l’ensemble des nœuds. Enfin, wsrep_cluster_address doit contenir les IP de tous les membres du cluster, séparés par une virgule.

[galera]
wsrep_on                 = ON
wsrep_cluster_name       = "MariaDB Galera Cluster"
wsrep_cluster_address    = gcomm://192.168.69.81,192.168.69.82,192.168.69.83
binlog_format            = row
default_storage_engine   = InnoDB
bind-address = 0.0.0.0
wsrep_slave_threads = 4
innodb_flush_log_at_trx_commit = 1
log_error = /var/log/mysql/error-galera.log

wsrep_slave_threads (wsrep_applier_threads) dans les versions plus récentes permet de gérer le nombre de threads pour traiter les write-sets, les transactions commités lors de la réplication. Généralement on souhaite avoir deux fois le nombre de cœurs du serveur pour des performances correctes.

Démarrage du cluster

La configuration précédente terminée, il est maintenant possible de démarrer le cluster. Un nœud doit être utilisé pour bootstraper le cluster avec la commande galera_new_cluster.

root@db01:~# galera_new_cluster

Cette commande initialise le Primary component du cluster et démarre Mariadb. Le primary Component est l’ensemble des nœuds qui forment le cluster, qui peuvent communiquer ensemble et assurer le commit d’une transaction. Chacun des nœuds démarrés par la suite se connecteront à ce nœud pour démarrer la réplication. En terminologie Galera, il s’agit du State Snapshot Transfert (SST).
Le journal de log définit plus tôt doit nous confirmer à ce propos que le bootstrap est lancé et que le statut du serveur passe en Joined.

2023-03-05 22:34:39 0 [Note] WSREP: Server status change initializing -> initialized
2023-03-05 22:34:39 0 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
2023-03-05 22:34:39 1 [Note] WSREP: Bootstrapping a new cluster, setting initial position to 00000000-0000-0000-0000-000000000000:-1
2023-03-05 22:34:39 4 [Note] WSREP: Cluster table is empty, not recovering transactions
2023-03-05 22:34:39 1 [Note] WSREP: Server status change initialized -> joined

Les autres nœuds du cluster peuvent ensuite être démarrés normalement. Ils vont se connecter au Primary Component, lancer un state transfert pour synchroniser la copie locale de la base de données afin d’être synchronisé au cluster.

root@db02:~# systemctl start mariadb

Contrôlons de nouveau nos logs, et l’on peut constaté que pas mal de lignes ont défilé. Ce que l’on doit retenir c’est que les nouveaux nœuds ont initialisé un state transfert depuis l’un des serveurs déjà synchronisés et que l’état doit être passé en « complete ».

2023-03-05 22:37:37 0 [Note] WSREP: Member 1.0 (db03) requested state transfer from '*any*'. Selected 0.0 (db02)(SYNCED) as donor.
2023-03-05 22:37:39 0 [Note] WSREP: (ea0f8bad-ad8c, 'tcp://0.0.0.0:4567') turning message relay requesting off
2023-03-05 22:37:39 0 [Note] WSREP: 0.0 (db02): State transfer to 1.0 (db03) complete.
2023-03-05 22:37:39 0 [Note] WSREP: Member 0.0 (db02) synced with group.
2023-03-05 22:37:42 0 [Note] WSREP: 1.0 (db03): State transfer from 0.0 (db02) complete.
2023-03-05 22:37:42 0 [Note] WSREP: Member 1.0 (db03) synced with group.

Chaque nœud peut ensuite être démarré ou redémarré indépendamment avec systemctl. Cependant, si chaque nœud a été arrêté proprement, le cluster n’est plus formé et il est nécessaire à nouveau de bootstrap le cluster avec galera_new_cluster.

Vous l’aurez deviné, en cas de maintenance, il conviendra de démarrer en premier le dernier nœud qui a été arrêté afin d’avoir des données cohérentes. Le problème va se poser si l’on ignore l’ordre d’arrêt ou que le cluster s’est mal arrêté. Il pourra dans ce cas être nécessaire d’indiquer que le nœud sur lequel on souhaite intialiser le bootstrap du cluster est en mesure de le prendre en charge, cela se fait dans le fichier /var/lib/mysql/grastate.dat avec la variable safe_to_bootstrap à forcer à 1. Cette valeur est à zéro après le bootstrap pour éviter une initialisation accidentelle d’un cluster déjà bootstrapé. De préférence, il s’agit d’une opération à utiliser en derniers recours.

root@db01:~# cat /var/lib/mysql/grastate.dat
# GALERA saved state
version: 2.1
uuid:    ea14ca85-bba5-11ed-bf58-0b029f897fa2
seqno:   -1
safe_to_bootstrap: 0

Tester la réplication

En théorie, notre cluster est en place. Pour le confirmer vérifions avec des données. Commençons par créer une base de données nommée galeratest qui ne comporte qu’une seule table :

root@db01:~# mysql -u root -e "CREATE DATABASE galeratest;"
root@db01:~# mysql -u root galeratest -e "CREATE TABLE test(ID int NOT NULL AUTO_INCREMENT, data varchar(255) NOT NULL, PRIMARY KEY (ID) );"

Peuplons maintenant cette table avec un millier d’enregistrements :

root@db01:~# for i in {1..1000}; do   mysql -u root galeratest -e "INSERT INTO test (\`data\`) VALUES ('test$i');"; done

Et enfin, vérifions sur un autre serveur que nous retrouvons bien nos enregistrements :

root@db02:~# mysql -u root galeratest -e "SELECT COUNT(ID) FROM test;"
+-----------+
| COUNT(ID) |
+-----------+
|      1000 |
+-----------+

C’est parfait, nous avons bien nos 1000 lignes dans la table, les données sont donc bien répliquées.
Superviser l’état du cluster

En environnement de production, il convient de vérifier que chaque nœud est connecté aux autres membres cluster et qu’il est bien répliqué. Ces opérations doivent être vérifiées à minima manuellement et ce régulièrement et idéalement automatiquement, que ce soit via des scripts ou une intégration de ceux-ci avec une solution de supervision type Nagios.

Galera permet de récupérer localement sur chaque nœud l’état du cluster avec un certain nombre de variables de statut. Les différentes variables wsrep du provider Galera sont accessibles au travers de SHOW STATUS, de manière similaire aux variables de statut de MariaDB.

A l’issue du déploiement précédent, wsrep_ready permet déjà de visualiser si le nœud est en mesure d’accepter les requêtes. Si la réponse est « OFF » le serveur renverra des erreurs aux clients.

MariaDB [mysql]> show status like 'wsrep_ready';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wsrep_ready   | ON    |
+---------------+-------+

Ensuite, wsrep_connected doit renvoyer ON. Si cette valeur renvoie OFF, alors le nœud n’est pas connecté au cluster. Cela peut être l’origine d’une erreur de configuration ou bien d’une anomalie réseau.

MariaDB [mysql]> show status like 'wsrep_connected';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| wsrep_connected | ON    |
+-----------------+-------+

Enfin, wsrep_cluster_size doit renvoyer le nombre de nœuds présents dans le cluster. En état nominal il s’agit logiquement du nombre de nœuds total du cluster. Toutefois cette valeur permet également de gérer les niveaux d’alertes pour lequel le nombre minimum de nœuds pour rendre le service. En théorie, un nœud suffit à rendre le service, toutes considérations de performances mises à part :

MariaDB [(none)]> show status like 'wsrep_cluster_size';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+

Ensuite, il faut vérifier que chaque nœud est connecté au primary component et donc qu’il est dans un état sain et dans un état où il peut recevoir des mises à jour des autres nœuds.

MariaDB [(none)]> show status like 'wsrep_cluster_status';
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| wsrep_cluster_status | Primary |
+----------------------+---------+

Pour aller plus loin dans la visualisation de l’état du nœud au sein du cluster, la variable wsrep_local_state_comment donne davantage d’indications sur l’état du nœud. Cette variable possède quatre valeurs :
• Joining : le nœud est en train de rejoindre le cluster
• Donor/Desynced : le nœud est la source de réplication d’un autre nœud en train de rejoindre le cluster
• Joined : le nœud a rejoint le cluster
• Synced : le nœud est synchronisé avec le cluster

MariaDB [(none)]> show status like 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name             | Value  |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+

Enfin, au-delà de l’état du cluster, il convient de superviser les performances de réplication. La variable wsrep_local_recv_queue_avg donne la longueur moyenne de la file d’attente de réplication. Si elle est supérieure à 0 cela signifie que le node a un peu de latence dans l’application des write-sets. Au delà de 0,5 il faut vérifier qu’il n’y a pas de goulot d’étranglement. S’agissant d’une valeur moyenne, il convient d’observer dans ce cas là les valeurs minimales et maximales, wsrep_local_recv_queue_min et wsrep_local_recv_queue_max afin d’avoir l’amplitude réelle.

MariaDB [(none)]> show status like 'wsrep_local_recv_queue_avg';
+----------------------------+----------+
| Variable_name              | Value    |
+----------------------------+----------+
| wsrep_local_recv_queue_avg | 0.142857 |
+----------------------------+----------+

Ces variables sont les principales à superviser. En pratique, il en existe une soixantaine visibles avec un « show status like ‘wsrep_%; ». La plupart ne seront d’aucune utilité mais celles présentées restent celles qu’il faut connaître impérativement.

Load balancing avec HAProxy

Notre cluster Galera est monté, les clients vont ensuite devoir s’y connecter. Si on configure les accès sur le nom d’hôte ou l’adresse IP d’un des nœuds, alors en cas d’indisponibilité de celui-ci les autres nœuds seront fonctionnels mais inutilisés. Une méthode consiste à utiliser du round robin DNS. C’est à dire que pour un nom d’hôte, on va déclarer trois enregistrements type A pointant vers chacuns des adresses IP des nœuds du cluster. Cependant cette solution n’est pas idéale et peut renvoyer vers un nœud non fonctionnel.

Une meilleure solution consiste à utiliser un load balancer qui va exposer le service MariaDB mais qui aura en backend les différents nœuds qui composent le cluster Galera. Le load balancer permet en outre de tester la disponibilité des backends et ne présente que les nœuds fonctionnels. HAProxy est le load balancer libre le plus populaire et le plus aboutit pour gérer ces situations. Commençons pas l’installation sur une VM dédiée.

root@lb:~# apt -y install haproxy
root@lb:~# systemctl enable haproxy

En suite, nous aurons besoin d’un utilisateur non privilégié qui permettra à HAProxy de s’authentifier pour vérifier qu’une connexion peut être établie.

MariaDB [mysql]> CREATE USER 'haproxy'@'192.168.69.70';

Enfin nous allons configurer HAProxy dans le fichier /etc/haproxy/haproxy.cfg et ajouter ces lignes à la fin du fichier.

listen galera
    bind 192.168.69.70:3306
    balance leastconn
    mode tcp
    option tcpka
    option mysql-check user haproxy
    option tcplog
    server db1 192.168.69.81:3306 check weight 1
    server db2 192.168.69.82:3306 check weight 1
    server db3 192.168.69.83:3306 check weight 1

Le service Galera est exposé sur le port MariaDB classique, 3306/TCP via l’IP du load balancer 192.168.69.70. Balance leastconn est l’algorithme de répartition de charge, dans ce cas il s’agit d’envoyer les connexions vers le serveur qui en a le moins, nous pourrions tout autant utiliser du round robin ou du source IP. Le check mysql-check est utilisé avec l’utilisateur haproxy créé précédemment. Haproxy enverra un paquet pour s’authentifier et un second pour mettre fin proprement à la connexion. Un simple check tcp engendrerait dans les logs une fin prématurée de la connexion. Enfin, on définit chaque nœud vers lequel on renvoie les connexions. Check sert à indiquer qu’on teste la disponibilité du backend et weight 1 que les connexions seront distribuées de manière équitable entre les backends.

Un redémarrage du service plus tard et on peut contrôler l’état de nos backends avec hatop. Si au moins un backend apparaît comme UP l’accès au travers de la VIP est possible.

root@lb:~# systemctl restart haproxy
root@lb:~# hatop -s /run/haproxy/admin.sock

Idéalement et dans un contexte de production, il faudrait également mettre en haute disponibilité le load balancer via une paire de load balancer HAProxy partageant une VIP en VRRP avec Keepalived par exemple. Ceci afin de ne pas créer de nouveau point unique de panne. Mais cela dépasse le cadre de cet article.

Conclusion

Galera est un moyen simple pour monter une solution de Haute Disponibilité pour vos bases de données MariaDB avec peu de contraintes d’exploitation. Il n’y a donc plus de raisons de réveiller les admins la nuit pour dépanner un serveur MariaDB en mono serveur dès lors que vous administrez des applications critiques. Pour aller plus loin, il s’agit d’un sujet qui se prête particulièrement bien à des déploiements orchestrés par Ansible ou Puppet.

Références
[BASEDOC] Base documentaire des Éditions Diamond : https://connect.ed-diamond.com/
[1] Documentation officielle de Galera : https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/
[2] Aller plus loin avec MariaDB, LP 130 par Masquelin Mickaël
[3] À la découverte d’HAProxy, LP HS 55 par Assmann Baptiste (bedis)

Déployer des environnements de développement avec Vagrant (Publié dans Linux Pratique n°127)

31 décembre 2024 à 10:48

Vagrant est un logiciel libre permettant de déployer rapidement des machines virtuelles. C’est un logiciel développé par la société Hashicorp, déjà connue pour d’autres logiciels comme Terraform, Packer ou Vault. Historiquement lié à VirtualBox, Vagrant s’est désormais largement ouvert à d’autres solutions de virtualisation comme Libvirt ou de conteneurisation comme Docker.

1 Bien démarrer

1.1 Pourquoi encore un autre outil ?

Vagrant permet de déployer rapidement des environnements à partir de fichiers de description, les Vagrantfile. S’agissant de fichiers textes, ils s’intègrent de fait à toute la chaine GIT ou CI/CD et donc aux bonnes pratiques de développement. L’utilisation d’un outil automatisé permet ainsi de gagner un temps précieux et sans valeur ajoutée sur l’instanciation de machines virtuelles.

L’automatisation du procédé permet également de produire des environnements similaires au sein d’une équipe et à chaque lancement de Vagrant. Les environnements de test sont donc reproductibles, gommant les différences qu’il peut y avoir selon les habitudes de travail et la dérive qui peut se créer sur un environnement au fil du temps.

A titre individuel c’est aussi un moyen de maquetter rapidement un projet ou un outil avant de se pencher sur une infrastructure de production et d’outil type Terraform, plus appropriés.

Vagrant offre une compatibilité avec plusieurs hyperviseurs : vmware, hyper-v mais sait également déployer des ressources localement via VirtualBox ou libvirt. C’est aussi un bon moyen de fournir des environnements de tests qui ne monopolisent pas les précieuses ressources des serveurs ou sans avoir à disposition d’infrastructure dédiée, qui plus est disponibles sans connexion réseau.

1.2 Installation

Sous ma Debian testing, vagrant est packagé par défaut donc un simple apt-get install suffit. Pour Ubuntu, Centos, Fedora, Hashicorp met à disposition des paquets adaptés à la distribution.
Pour les autres, un zip contient un binaire à pousser dans un répertoire appelé dans votre PATH. Vagrant, c’est tout simplement ça, tout un workflow unifié autour d’une même CLI.
Un Virtualbox fonctionnel sera nécessaire pour la suite, avec votre utilisateur dans le bon groupe pour éviter de devoir jouer du sudo.

1.3 Les boxes

Les boxes Vagrant sont des images de systèmes déployables au sein de tout environnement Vagrant. Il s’agit en pratique d’une façon de packager une distribution afin de permettre un déploiement uniforme quel que soit le système d’exploitation ou l’hyperviseur utilisé. En pratique, toutes les box ne sont pas compatibles avec tous les hyperviseurs, à plus forte raison lorsqu’il s’agit d’une image docker.

De nombreuses boxes sont disponibles sur le site https://app.vagrantup.com et certaines sont officiellement développées par les distributions majeures ou certains éditeurs. Les seules boxes officielles sont toutefois limitées aux boxes Hashicorp en Bento. Un système de versionning permet également de s’assurer de déployer sur une version connue et validée.

Pour télécharger une box, il suffit d’un vagrant box add suivi du nom de la box :

vagrant box add centos/7
==> box: Loading metadata for box 'centos/7'
    box: URL: https://vagrantcloud.com/centos/7
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.
 
1) hyperv
2) libvirt
3) virtualbox
4) vmware_desktop
 
Enter your choice: 3
==> box: Adding box 'centos/7' (v2004.01) for provider: virtualbox
    box: Downloading: https://vagrantcloud.com/centos/boxes/7/versions/2004.01/providers/virtualbox.box
Download redirected to host: cloud.centos.org
    box: Calculating and comparing box checksum...
==> box: Successfully added box 'centos/7' (v2004.01) for 'virtualbox'!

Et pour lister les boxes installées :

vagrant box list
centos/7 (virtualbox, 2004.01)
ubuntu/focal64 (virtualbox, 20210513.0.0)

Vous l’aurez compris donc pour la logique sous jacente, il y a donc notamment les commandes update, remove (et d’autres) pour suivre le cycle de vie des boxes locales.

1.4 Le workflow avec Vagrant

Vagrant va chercher à provisionner le type de machine en fonction de ce qui est décrit dans le fichier Vagrantfile du répertoire du projet. Un fichier Vagrantfile est un fichier Ruby mais qui ne nécessite aucune connaissance de ce langage.
Le fichier Vagrantfile sert à décrire tout ce qui constitue le déploiement : le provider, le dimensionnement de la VM et éventuellement la post-installation. S’agissant d’un fichier texte, il a toute sa place sur un dépôt Git.
Voyons un premier exemple de Vagrantfile. A minima, Vagrant a besoin du nom du déploiement et du type d’image source. Le « 2 » est simplement la version du fichier de configuration.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
end

Vérifions ensuite que notre syntaxe est sans erreur :

vagrant validate
Vagrantfile validated successfully.

S’il n’y a pas d’erreur, on peut lancer l’instanciation de notre projet :

vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/focal64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/focal64' version '20210513.0.0' is up to date...
==> default: Setting the name of the VM: 1simple_default_1621158383628_85938
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Setting hostname...
==> default: Mounting shared folders...
    default: /vagrant => /home/julien/vagrant/1simple

L’état du déploiement peut être visualisé avec vagrant status :

vagrant status
Current machine states:
default running (virtualbox)
[…]

A l’instanciation du projet, une paire de clé SSH est créée. Elle est stockée dans l’arborescence du projet vagrant dans .vagrant/machines/NOM_BOX/virtualbox/private_key permettant de se connecter à l’instance sans mot de passe.

vagrant ssh default
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-73-generic x86_64)
[...]
Last login: Sun May 16 09:54:12 2021 from 10.0.2.2
vagrant@vagrantbox:~$ logout

En fin de cycle, les VM sont ensuite arrêtées et le projet détruit. Lorsque le déploiement n’implique qu’une seule instance, il n’est pas nécessaire de préciser le nom de la VM.

vagrant halt default
==> default: Attempting graceful shutdown of VM...
vagrant destroy
    default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Destroying VM and associated drives...

Ainsi, le fait de relancer la commande vagrant up va permettre de provisionner un environnement neuf et propre à chaque cycle.
Pour ne pas partir de zéro et avoir un modèle de fichier Vagrant à personnaliser, nous aurions pu partir de la commande vagrant init. En effet, celle-ci créé un fichier Vagrantfile dans le dossier courant qui peut être ensuite personnalisé selon ses souhaits.

1.5 Personnalisation

Notre premier exemple servait à mettre le pied à l’étrier sur le cycle de vie d’un environnement Vagrant. Cependant, il sera systématiquement ou presque nécessaire de configurer l’environnement à déployer en fonction des ressources disponibles sur la machine qui héberge les machines virtuelles ou encore en fonction des besoins réels des applicatifs.

Pour cela, Vagrant expose certaines fonctionnalités du provider ou un jeu de fonctionnalités communes. Par exemple, je vais avoir besoin d’accéder à mon serveur Web de développement donc je redirige le port local non privilégié 8080 afin de pouvoir accéder à ma VM via http://localhost:8080.

Plus spécifiquement pour Virtualbox, via l’attribut customize il est possible d’exposer n’importe quel paramètre de la commande VboxManage. D’un côté cela limite la qualité de la couche d’abstraction fournie par le provider Virtualbox pour Vagrant mais de l’autre celui-ci n’en limite pas les possibilités.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
  config.vm.network "private_network", ip: "192.168.56.10", virtualbox__intnet: "nat"

  config.vm.provider :virtualbox do |vbox|
    vbox.gui = false
    vbox.linked_clone = false
    vbox.memory = 2048
    vbox.cpus = 2
    vbox.customize ["modifyvm", :id, "--uart1", "0x3F8", 4]
  end
end

J’ai également tendance à utiliser les clones liés afin de limiter l’impact sur la consommation des disques mais ce point est loin d’être obligatoire.

1.6 Multi machines

Les quelques déploiements précédents étaient tous tournés autour d’une unique VM. Cependant, les infrastructures de production vont plutôt être déployées sur plusieurs serveurs pour :
– Répondre à des schémas d’architecture multi-tiers
– Combiner plusieurs systèmes d’exploitation
– Distribuer la charge ou la disponibilité du service sur plusieurs machines

Reproduire ce type d’infrastructure sur une seule machine reviendrait à construire un environnement dev/test différent de la production et donc ne pas couvrir les cas listés ci-dessus. Vagrant est capable de déployer plusieurs VM au sein d’un même projet avec le même workflow. Il suffit pour cela de lister successivement les VM à déployer :

Vagrant.configure("2") do |config|
  config.vm.define "revproxy" do |host|
    host.vm.box = "revproxy"
    host.vm.box = "ubuntu/focal64"
  end
  config.vm.define "www", primary: true do |host|
    host.vm.box = "www"
    host.vm.box = "ubuntu/focal64"
  end
  config.vm.define "db" do |host|
    host.vm.box = "db"
    host.vm.box = "ubuntu/focal64"
  end
end

Pour provisionner l’infrastructure, les mêmes commandes s’appliquent. Cependant, si l’on souhaite simplement instancier l’un des serveurs de l’infrastructure, par exemple le serveur de base de données, il est possible de préciser celui-ci via vagrant up db puis vagrant ssh db pour s’y connecter.

Ce listing de VM est un peu ennuyeux cependant, voyons comment factoriser cela en utilisant une variable Ruby de type Symbol :

hosts = [
  { :hostname => 'revproxy', :ip => '192.168.56.10', :mem => 1024, :cpu => 1 },
  { :hostname => 'www',   :ip => '192.168.56.11', :mem => 2048, :cpu => 2 },
  { :hostname => 'db',   :ip => '192.168.56.12', :mem => 2048, :cpu => 2 },
]
 
 
Vagrant.configure("2") do |config|
  hosts.each do |host|
    config.vm.define host[:hostname] do |hostconfig|
      hostconfig.vm.box = "ubuntu/focal64"
      hostconfig.vm.hostname = host[:hostname]
      hostconfig.vm.network :private_network, ip: host[:ip]
      hostconfig.vm.provider :virtualbox do |vbox|
        vbox.gui = false
        vbox.linked_clone = true
        vbox.memory = host[:mem]
        vbox.cpus = host[:cpu]
      end
    end
  end
end

Lorsque plusieurs projets comme ces deux versions du multi-instances sont démarrés, il est possible de les visualiser via la commande vagrant global-status. Cela donne également le chemin racine du projet Vagrant sur le système de fichiers.

vagrant global-status
id name provider state directory
-------------------------------------------------------------------------
36e198a revproxy virtualbox running /home/julien/vagrant/6multi
971634f www virtualbox running /home/julien/vagrant/6multi
5f4e571 db virtualbox running /home/julien/vagrant/6multi
9008b27 revproxy virtualbox running /home/julien/vagrant/7multi
88fcf71 www virtualbox running /home/julien/vagrant/7multi
177b8c7 db virtualbox running /home/julien/vagrant/7multi

1.7 Les triggers

Lorsque l’on provisionne un environnement, il peut être nécessaire de déclencher des actions avant ou après le provisionnement d’une VM. Vagrant permet d’utiliser des triggers avant ou après certains états : up, destroy, reload, all, etc.
Les commandes peuvent être lancées aussi bien sur l’hôte que sur l’invité. Dans l’exemple ci-dessous à chaque fois que je provisionne ma VM, je récupère le code à jour depuis le dépôt GIT et lorsque je supprime celle-ci, je réalise une sauvegarde de la base de données.

Bon à savoir, par défaut l’exécution de Vagrant échoue si un trigger échoue mais ce comportement peut être modifié.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
  
  config.trigger.after :up do |trigger|
    trigger.run = {inline: "git clone https://github.com/WordPress/WordPress.git"}
    trigger.on_error = :continue
  end
  config.trigger.before :destroy do |trigger|
    trigger.run_remote = {inline: "mysqldump -u root wordpress > /vagrant/backup.sql"}
  end
end

1.8 Volumes synchronisés

Pour permettre l’échange de données entre l’hôte et les VM invitées, Vagrant permet de monter un volume sur la VM. Le volume peut être aussi bien un dossier local qu’un montage d’un système de fichiers réseau comme du NFS ou du CIFS.
Vagrant supporte également les options de montage que l’on peut passer à la commande mount. Dans l’exemple ci-dessous, je monte le répertoire html de mon home dans le répertoire par défaut d’Apache en remappant les droits. Si le dossier /home/julien/html n’existe pas, il est créé.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
  config.vm.synced_folder "/home/julien/html/", "/var/www/html", owner: "apache", group: "apache"
  create: true
end

2 Les Provisionners

2.1 Provisionner Shell

Provisionner des VM, c’est sympa me direz-vous mais s’il reste tout le travail de personnalisation des systèmes, nous n’avons guère fait plus que cloner une VM. Une fois que les VM sont provisionnées depuis un modèle de box, c’est un OS vierge qui est fourni quand on arrive au vagrant SSH. Bien entendu rien n’exclue de faire l’installation à suivre à la main mais ce serait dommage.
Les provisionners sont appelés à deux moments par Vagrant, à l’instanciation avec un vagrant up ou bien avec un vagrant provision pour une instance déjà lancée. Dans l’exemple-ci dessous, vagrant déploie les mises à jour via le shell ainsi que le minimum de packages que j’attends sur mes instances.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
  config.vm.network "private_network", ip: "192.168.56.10", virtualbox__intnet: "nat"
  config.vm.network "forwarded_port", guest: 80, host: 8080
  
  config.vm.provider :virtualbox do |vbox|
    vbox.gui = false
    vbox.memory = 2048
    vbox.cpus = 2
  end
  
  config.vm.provision "shell", inline: <<-SHELL
    apt update
    apt -y dist-upgrade
    apt -y install screen htop nmap
  SHELL
  
end

2.2 Provisionner Ansible

Bash n’est pas le seul provisionner supporté par Vagrant. En pratique, le support est même très complet car Vagrant permet d’utiliser la plupart des outils de gestion de configuration qui existent sous Linux : Chef, Puppet, Salt, CFEngine et Ansible ou même cloud-init.

A titre personnel, je n’ai d’expérience qu’avec Puppet et Ansible, je vous propose de partir sur Ansible celui-ci étant désormais largement plus répandu. Pour ansible, partons du petit playbook ci-dessous. Celui-ci déploie les mises à jour et installe un serveur web apache, tout ce qu’il y a de plus classique.

---
- hosts: all
  become: true
  gather_facts: true
  tasks:
    - name: apt upgrade
      apt:
        upgrade: dist
        update_cache: yes
        autoclean: yes
        autoremove: yes
    - name: install apache
      apt:
        name=apache2
        state=present
    - name: Enable service Apache2
      service:
        name: apache2
        state: started
        enabled: yes

Le provisionner ansible nécessite l’installation d’ansible sur la machine qui lance Vagrant. De la manière la plus simple, il suffit d’indiquer le nom du playbook à lancer. Mon playbook est volontairement succinct car ce n’est pas ici le propos mais rien n’interdit des déploiements plus complexes avec des rôles ou l’utilisation d’un vault par exemple qui sont aussi plus représentatifs des usages réels d’ansible.

Pour ceux qui ont l’habitude d’ansible, vous aurez remarqué l’absence notable de l’utilisation d’un inventaire. En pratique, il est possible d’en spécifier un dans la configuration mais Vagrant a l’intelligence lorsqu’aucun inventaire n’est spécifié d’en auto-générer un pour nous.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.hostname = "vagrantbox"
  config.vm.network "private_network", ip: "192.168.56.10", virtualbox__intnet: "nat"
  config.vm.network "forwarded_port", guest: 80, host: 8080
  
  config.vm.provider :virtualbox do |vbox|
    vbox.gui = false
    vbox.linked_clone = true
    vbox.memory = 1024
    vbox.cpus = 1
  end
  
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "playbook.yml"
    ansible.compatibility_mode = "2.0"
  end
 
end

3 Et pour d’autres providers ?

3.1 Quel intérêt ?

Utiliser Vagrant avec un autre provider a un premier intérêt certain c’est que le même outil sera utilisé quel que soit l’environnement mis à disposition ou le projet sous-jacent.
Ainsi en terme de formation ou de prise en main, la courbe d’apprentissage est plus faible que d’apprendre un autre outil.

3.2 Docker

Comme je disais au départ, Vagrant a pour objectif d’être relativement agnostic vis à vis de la solution de virtualisation. Il existe par exemple un provider docker officiel. Dans le code ci-après, je suis parti de l’exemple du Dockerhub pour instancier un cluster WordPress avec Docker compose mais remis à la sauce Vagrant.

L’image est ici automatiquement téléchargée depuis la registry. Dans le cadre d’une utilisation en mode développement, Vagrant est également capable d’aller construire l’image si on lui spécifie à la plage de l’image le chemin vers le Dockerfile, avec l’attribut build_dir, voire même depuis un dépôt git en utilisant l’attribut git_repo. Naturellement ces trois choix sont exclusifs.

ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker'
Vagrant.configure("2") do |config|
 
  config.vm.define "db" do |db|
    db.vm.synced_folder ".", "/vagrant", disabled: true
    db.vm.provider "docker" do |d|
      d.image = "mysql:5.7"
      d.name = "wordpressdb"
      d.remains_running = true
      d.volumes = ["/home/julien/mysql:/var/lib/mysql"]
      d.env = {"MYSQL_DATABASE" => "exampledb", "MYSQL_USER" => "exampleuser", "MYSQL_PASSWORD" => "examplepass", "MYSQL_RANDOM_ROOT_PASSWORD" => "'1'"}
    end
  end
  
  config.vm.define "app" do |app|
    app.vm.synced_folder ".", "/vagrant", disabled: true
    app.vm.provider "docker" do |d|
      d.image = "wordpress"
      d.ports = ["8080:80"]
      d.name = "wordpressapp"
      d.remains_running = true
      d.volumes = ["/home/julien/html:/var/www/html"]
      d.env = {"WORDPRESS_DB_HOST" => "db", "WORDPRESS_DB_USER" => "exampleuser", "WORDPRESS_DB_PASSWORD" => "examplepass", "WORDPRESS_DB_NAME" => "exampledb"}
      d.link("wordpressdb:mysql")
    end
  end
end

Dans ce cas présent, mon conteneur web est lié au conteneur de base de données, il faut donc que vagrant ne lance pas l’instanciation en parallèle des conteneurs, on y ajoute donc le paramètre –no-parallel.

vagrant up --no-parallel
Bringing machine 'db' up with 'docker' provider...
Bringing machine 'app' up with 'docker' provider...
==> db: Creating and configuring docker networks...
==> db: Creating the container...
    db:   Name: wordpressdb
    db: Image: mysql:5.7
    db: Volume: /home/julien/mysql:/var/lib/mysql
    db:
    db: Container created: 30dba7edabca0db7
==> db: Enabling network interfaces...
==> db: Starting container...
==> app: Creating and configuring docker networks...
==> app: Creating the container...
    app:   Name: wordpressapp
    app: Image: wordpress
    app: Volume: /home/julien/html:/var/www/html
    app:   Port: 8080:80
    app:   Link: wordpressdb:mysql
    app:
    app: Container created: 19838387018b928d
==> app: Enabling network interfaces...
==> app: Starting container...

Vérifions du côté de la CLI Docker qu’il n’y ait pas d’erreur. Parfait, nos conteneurs sont correctement instanciés.

docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                  NAMES
19838387018b   wordpress   "docker-entrypoint.s…"   10 seconds ago   Up 9 seconds    0.0.0.0:8080->80/tcp   wordpressapp
30dba7edabca   mysql:5.7   "docker-entrypoint.s…"   11 seconds ago   Up 10 seconds   3306/tcp, 33060/tcp    wordpressdb

Conclusion

Si vous utilisiez un hyperviseur comme virtualbox ou libvirt pour déployer des VM à la main et si vous avez besoin de régulièrement recréer vos environnements de développement, Vagrant devrait beaucoup apporter à votre trousse à outils. Et pour la production, Terraform est l’étape suivante si vous ne l’utilisez pas encore.
Références

[1] https://www.vagrantup.com/downloads
[2] https://app.vagrantup.com/boxes/search
[3] https://www.vagrantup.com/docs

Gérez votre stockage avec Stratis (Publié dans Linux Pratique N°128)

27 mai 2024 à 20:05

Quand on parle de gestion du stockage local à un administrateur système Linux, il est généralement question de LVM et de filesystem XFS ou EXT4. A un niveau plus évolué, on trouvera ZFS qui n’est pas libre ou bien BTRFS qui tarde à tenir ses promesses. Stratis a pour vocation de proposer quelque chose de neuf avec l’existant !
Lors de la sortie de la RHEL8, RedHat a annoncé Stratis qui permet une gestion moderne et avancée du stockage proche de ce qu’offre ZFS. Cependant à la différence des filesystems traditionnels opérant avec un module kernel et une CLI, Stratis se base sur les briques existantes, device-mapper et XFS. Ces briques étant matures et éprouvées depuis des années et stratis opérant en espace utilisateur, cela a permis de nettement accélérer le développement.

Par gestion moderne du stockage, concrètement j’entends par là :

  • Une gestion basée sur des pool
  • Les snapshots de filesystem
  • Le thin provisionning
  • La gestion de cache FS
  • L’utilisation d’une CLI homogène pour l’ensemble des opérations

1 Installation

Stratis ayant été développé pour RHEL8 et placé sous licence libre MPL 2.0, il est naturellement disponible pour l’ensemble des dérivés de celle-ci, CentOS, OracleLinux et pour ma part, RockyLinux ainsi que pour Fedora.
Pour l’installation, c’est très simple, s’agissant d’un démon userland, une simple installation de paquet et l’activation d’un service systemd suffisent.
[root@rocky ~]# dnf install stratisd stratis-cli
[root@rocky ~]# systemctl start stratisd.service
[root@rocky ~]# systemctl status stratisd.service

C’est tout, il n’y a pas de module kernel ou de fichier de configuration à modifier. Toutes les interactions se font au travers de la CLI.

Création d’un pool
Les disques gérés par stratis doivent être tout d’abord ajoutés à un pool. Ces disques ne doivent être rattachés à aucune configuration de stockage et n’avoir aucune table de partition.
Dans cet exemple, j’ai ajouté quatre disques vierges, vérifions avec lsblk :

[root@rocky ~]# lsblk -p
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
/dev/sda 8:0 0 32G 0 disk
├─/dev/sda1 8:1 0 1G 0 part /boot
└─/dev/sda2 8:2 0 31G 0 part
├─/dev/mapper/rl-root 253:0 0 29G 0 lvm /
└─/dev/mapper/rl-swap 253:1 0 2.1G 0 lvm [SWAP]
/dev/sdb 8:16 0 32G 0 disk
/dev/sdc 8:32 0 32G 0 disk
/dev/sdd 8:48 0 32G 0 disk
/dev/sde 8:64 0 32G 0 disk
/dev/sr0 11:0 1 1.9G 0 rom

Dans le cas où des données sont présentes sur les disques, il est possible de faire le nettoyage avec la commande wipefs et bien entendu après avoir préalablement vérifié que les données présentes ne sont plus nécessaires.
Créons donc un pool avec deux disques. Il est tout à fait possible de créer un pool à un seul disque. La redondance n’est d’ailleurs pas encore gérée par stratis.
[root@rocky ~]# stratis pool create stratispool /dev/sdb /dev/sdc
Vérifions que le le pool a bien été créé :
[root@rocky ~]# stratis pool list
Name Total Physical Properties
stratispool 64 GiB / 46.45 MiB / 63.95 GiB ~Ca,~Cr

Les properties affichées signifient que le pool n’a pas de cache (~Ca) et n’est pas chiffré (~Cr).
Vérifions ensuite nos disques. En principe, ils sont désormais gérés par stratis et sont identifiés comme tel :
[root@rocky ~]# stratis blockdev list
Pool Name Device Node Physical Size Tier
stratispool /dev/sdb 32 GiB Data
stratispool /dev/sdc 32 GiB Data

Effectivement, les deux disques ajoutés au pool sont désormais rattachés au pool stratispool. Aucun des autres disques qui n’ont pas été ajoutés à un pool ne sont listés. Ils ne sont en effet pas gérés par stratis. Les commandes classiques s’appliquent donc pour ces derniers.
Naturellement, l’ensemble des périphériques bloc d’un même pool doivent être de même type afin d’avoir une consistance sur les I/O. Un même pool ne doit pas être constitué de périphériques bloc SSD et SATA 7200 par exemple.

2 Création d’un système de fichiers

Nous avons créé précédemment un pool de stockage nous pouvons désormais créer un filesystem s’appuyant sur ce pool. Le choix du type de FS est imposé, il s’agit forcément de XFS.
[root@rocky ~]# stratis fs create stratispool data
Vérifions :
[root@rocky ~]# stratis fs list
Pool Name Name Used Created Device UUID
stratispool data 546 MiB Sep 12 2021 05:21 /dev/stratis/stratispool/data 9f1f993f780c40f795ed28fca79c3b18

Vous avez dû immédiatement remarquer quelque chose. En effet, à aucun moment nous n’avons précisé de taille sur notre système de fichiers. Avec stratis, la règle est l’utilisation du thin provisionning. C’est à dire que les blocs ne sont pas réservés sur les périphériques sous-jacents mais alloués à la volée. Lorsque le FS est proche d’une saturation, une extension de celui-ci est alors réalisée.
C’est pour cette raison qu’il est indispensable de ne jamais manipuler un filesystem en dehors de stratis. D’ailleurs lsblk affiche les différentes couches utilisées en interne :
[root@rocky ~]# lsblk /dev/sdb /dev/sdc
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sdb 8:16 0 32G 0 disk
└─stratis-1-private-c5537a12176a482e8df8af23ebdce463-physical-originsub 253:2 0 64G 0 stratis
├─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-thinmeta 253:3 0 64M 0 stratis
│ └─stratis-1-private-c5537a12176a482e8df8af23ebdce463-thinpool-pool 253:6 0 63.9G 0 stratis
│ └─stratis-1-c5537a12176a482e8df8af23ebdce463-thin-fs-9f1f993f780c40f795ed28fca79c3b18 253:7 0 1T 0 stratis
├─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-thindata 253:4 0 63.9G 0 stratis
│ └─stratis-1-private-c5537a12176a482e8df8af23ebdce463-thinpool-pool 253:6 0 63.9G 0 stratis
│ └─stratis-1-c5537a12176a482e8df8af23ebdce463-thin-fs-9f1f993f780c40f795ed28fca79c3b18 253:7 0 1T 0 stratis
└─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-mdv 253:5 0 16M 0 stratis
sdc 8:32 0 32G 0 disk
└─stratis-1-private-c5537a12176a482e8df8af23ebdce463-physical-originsub 253:2 0 64G 0 stratis
├─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-thinmeta 253:3 0 64M 0 stratis
│ └─stratis-1-private-c5537a12176a482e8df8af23ebdce463-thinpool-pool 253:6 0 63.9G 0 stratis
│ └─stratis-1-c5537a12176a482e8df8af23ebdce463-thin-fs-9f1f993f780c40f795ed28fca79c3b18 253:7 0 1T 0 stratis
├─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-thindata 253:4 0 63.9G 0 stratis
│ └─stratis-1-private-c5537a12176a482e8df8af23ebdce463-thinpool-pool 253:6 0 63.9G 0 stratis
│ └─stratis-1-c5537a12176a482e8df8af23ebdce463-thin-fs-9f1f993f780c40f795ed28fca79c3b18 253:7 0 1T 0 stratis
└─stratis-1-private-c5537a12176a482e8df8af23ebdce463-flex-mdv 253:5 0 16M 0 stratis

3 Montage d’un système de fichiers

Le montage d’un système de fichiers dans l’arborescence est somme toute classique et se déroule exactement de la même façon qu’avec n’importe quel autre système de fichiers.
[root@rocky ~]# mkdir /mnt/data
[root@rocky ~]# mount /dev/stratis/stratispool/data /mnt/data
[root@rocky ~]# df -h /mnt/data
Filesystem Size Used Avail Use% M
/dev/mapper/stratis-1-c5537a12176a482e8df8af23ebdce463-thin-fs-9f1f993f780c40f795ed28fca79c3b18 1.0T 7.2G 1017G 1% /

Pour rendre permanent le montage, je recommande néanmoins de récupérer l’UUID du périphérique et d’utiliser celui-ci pour configurer le fichier /etc/fstab.
Pour le récupérer, encore une fois la commande lsblk est là pour nous aider :
root@rocky ~]# lsblk -o uuid /dev/stratis/stratispool/data
UUID
9f1f993f-780c-40f7-95ed-28fca79c3b18
[root@rocky ~]# echo "UUID=9f1f993f-780c-40f7-95ed-28fca79c3b18 /mnt/data xfs defaults 0 0" >> /etc/fstab

4 Etendre le pool

Avec la croissance des données, il arrivera forcément un moment où l’extension du pool sera nécessaire.
Pour cela, il suffit d’ajouter un disque au pool :
[root@rocky ~]# stratis pool add-data stratispool /dev/sdd

Et contrôler que le pool avec bien été étendu de la volumétrie souhaitée :
[root@rocky ~]# stratis pool list
Name Total Physical Properties
stratispool 96 GiB / 599.68 MiB / 95.41 GiB ~Ca,~Cr

Le pool est étendu, il n’y a toujours pas d’action à prévoir côté système de fichiers. Stratis se chargera de redimensionner le FS en fonction de l’espace disponible sur le pool. Note, il n’est pas possible de définir une taille maximale pour un système de fichiers au sein d’un pool. Dans ce cas, il est nécessaire de créer un pool dédié.

5 Snapshots

Un snapshot est une copie extracte à un instant donné d’un système de fichiers. Ceux-ci peuvent être pris à chaud, sans démontage du filesystem sur lequel un instantané doit être pris.
Un snapshot stratis peut être ensuite monté en lecture/écriture et a ensuite une existance totalement indépendant du FS d’origine. Un snapshot stratis ne fonctionne donc pas tout à fait comme un snapshot de machine virtuelle.
Pour créer un snapshot de notre précédent FS, il suffit de :
[root@rocky ~]# stratis fs snapshot stratispool data data_snap
Comme je disais, un snapshot étant un FS indépendant du FS original, pour lister les snapshots il faut en pratique prendre la liste des FS :
[root@rocky ~]# stratis fs list
Pool Name Name Used Created Device UUID
stratispool data_snap 546 MiB Sep 12 2021 09:33 /dev/stratis/stratispool/data_snap eb3ca4299bb34d8993260d7af087c21e
stratispool data 546 MiB Sep 12 2021 09:32 /dev/stratis/stratispool/data 5a9c2077ec6a4793a361587938045154
Le snapshot peut ensuite être monté comme n'importe quel filesystem :
[root@rocky ~]# mkdir /mnt/data_snap
[root@rocky ~]# mount /dev/stratis/stratispool/data_snap /mnt/data_snap/

En ce qui concerne le retour arrière sur snapshot il n’existe pas de procédure automatique. Vous l’aurez deviné, il faut donc arrêter les processus qui écrivent sur le système de fichiers, le démonter et monter le snapshot à la place.
Pour faire propre, je vous recommande de renommer le snapshot afin de ne pas garder un nom incohérent avec l’environnement de production puis de supprimer le système de fichier inutile.

6 Cache

Stratis a la capacité de proposer un cache disque. Cela n’est utile en pratique que dans le cas où plusieurs technologies disques sont présentes. Par exemple votre stockage nominal est constitué de disques SAS et vous disposez de disques flash NVME. Stratis permet de configurer un cache sur le stockage flash afin d’augmenter sensiblement les performances du système de fichiers.
Pour ajouter un disque comme cache à un volume stratis, on passe par la commande init-cache.
[root@rocky ~]# stratis pool init-cache stratispool /dev/sde
L’ajout du cache au pool modifie ainsi les propriétés du pool. La propriété Ca pert ainsi le caractère ~ qui constitue une négation au niveau de l’affichage :
[root@rocky ~]# stratis pool list
Name Total Physical Properties
stratispool 96 GiB / 599.68 MiB / 95.41 GiB Ca,~Cr

En affichant la liste des périphériques bloc gérés par stratis, on trouve désormais nos trois disques initiaux flaggés comme disques de données, et le disque que l’on vient d’ajouter comme disque de cache :
[root@rocky ~]# stratis blockdev list
Pool Name Device Node Physical Size Tier
stratispool /dev/sdb 32 GiB Data
stratispool /dev/sdc 32 GiB Data
stratispool /dev/sdd 32 GiB Data
stratispool /dev/sde 32 GiB Cache

7 Nettoyage

Notre environnement de test étant terminé, procédons au nettoyage. Premièrement, on démonte les deux FS. Il faudra également si nécessaire retirer les entrées associées dans le fichier /etc/fstab :
[root@rocky ~]# umount /mnt/data_snap
[root@rocky ~]# umount /mnt/data

On peut ensuite supprimer les systèmes de fichier :
[root@rocky ~]# stratis fs destroy stratispool data_snap
[root@rocky ~]# stratis fs destroy stratispool data

Et enfin, on peut supprimer le pool :
[root@rocky ~]# stratis pool destroy stratispool

On peut vérifier le suppression du pool en contrôlant que les disques ne sont plus attachés à aucun pool :
[root@rocky ~]# stratis blockdev list
Pool Name Device Node Physical Size Tier

8 Chiffrement

Stratis supporte le chiffrement de pool à la création. Ce qui signifie donc qu’il n’est pas possible de chiffrer un volume à posteriori. S’agissant d’un chiffrement au niveau du pool, tous les FS basés sur ce pool se retrouvent donc automatiquement chiffrés.
En premier lieu, il faut générer une clé de chiffrement. Voici comment en générer une depuis l’entrée standard et vérifier qu’elle soit bien connue de stratis :
[root@rocky ~]# stratis key set --capture-key poolkey
Enter key data followed by the return key:
[root@rocky ~]# stratis key list
Key Description
poolkey

Il est désormais possible de créer le pool chiffré avec cette clé et un FS sur ce pool :
[root@rocky ~]# stratis pool create --key-desc poolkey poolcrypt /dev/sdd
[root@rocky ~]# stratis pool list
Name Total Physical Properties
poolcrypt 31.98 GiB / 39.24 MiB / 31.95 GiB ~Ca, Cr
[root@rocky ~]# stratis filesystem create poolcrypt data
[root@rocky ~]# stratis fs list
Pool Name Name Used Created Device UUID
poolcrypt data 546 MiB Sep 12 2021 12:54 /dev/stratis/poolcrypt/data 60eae29bf1fd48b6ad25fd2eeba7ce2f

La gestion du chiffrement des pool a cependant un gros défaut, elle se fait en mémoire. En effet, après un redémarrage du système d’exploitation la clé n’est pas connue de manière persistante par stratis et doit de nouveau lui être fournie. Par conséquent, les volumes ne seront pas réactivés au démarrage. Pour ceci une des solutions est de gérer le cas avec systemd après avoir placé la clé dans un fichier protégé par les bons droits d’accès :
[root@rocky ~]# echo "secret" > /root/poolkey
[root@rocky ~]# chmod 0400 /root/poolkey && chown root:root /root/poolkey

Il reste à définir un unit systemd qui chargera la clé dans stratis, qui activera le pool et qui montera le filesystem :
[Unit]
Description = Montage de /data
After = stratisd.service

[Service]
ExecStartPre=stratis key set --keyfile-path /root/poolkey poolkey && stratis pool unlock keyring
ExecStart=mount /dev/stratis/poolcrypt/data /data
RemainAfterExit=yes

[Install]
WantedBy = multi-user.target

Enfin, il ne reste plus qu’à activer notre nouvel unit :
[root@rocky ~]# systemctl enable stratis-data
Created symlink /etc/systemd/system/multi-user.target.wants/stratis-data.service → /etc/systemd/system/stratis-data.service.

Conclusion

Stratis est assez jeune, c’est encore en Technological Preview chez RedHat. Néanmoins, sa conception simple et son interface en ligne de commande simple et cohérente sont appréciables.
Bien entendu, l’ensemble des opérations d’exploitation du stockage dont un sysadmin a besoin au quotidien sont pour l’instant très loin de toutes être gérées, mais l’outil est très prometteur et répond à un vrai besoin de simplification.

Références
[1] Site officiel de Stratis : https://stratis-storage.github.io
[2] La conception de Stratis expliquée : https://stratis-storage.github.io/StratisSoftwareDesign.pdf

Fabric, le couteau suisse de l’automatisation (Publié dans Linux Pratique 122)

4 avril 2024 à 20:25

Fabric est une bibliothèque Python et une interface en ligne de commande facilitant l’utilisation de SSH, que ce soit pour des applications ou dans le but d’automatiser certaines tâches répétitives d’administration système. La grande force de Fabric est d’être particulièrement simple à utiliser.

1 Fabric, késako?

Fabric, c’est tout le contraire des outils DevOps modernes tels que Salt ou Ansible pour ne citer que ceux qui opèrent au dessus du protocole SSH. Ce n’est pas un langage de haut niveau avec de nombreux modules faisant abstraction des couches systèmes inférieures. Point d’idempotence, de templates ou toutes les fonctionnalités qui font des outils de gestion de configuration le saint Graal de l’orchestration et pourtant je suis un fervent partisant de Puppet.
Pour autant, lorsqu’il s’agit d’interagir rapidement sur un grand nombre de serveurs, Puppet n’est pas la solution la plus efficace et cluster SSH encore moins. L’adoption de Fabric dans mon entreprise lorsque le produit est devenu mature a radicalement changé notre façon d’administrer nos nombreux systèmes en parallèle de l’utilisation de Puppet. Il m’est ainsi arrivé fréquemment d’utiliser Fabric pour bootstraper des agents Puppet.
Lorsque l’on écrit du code Fabric, on travaille avec les primitives natives de SSH et le shell et on fait moins qu’effleurer du Python. C’est cette simplicité qui fait de Fabric un véritable couteau suisse, utilisable par n’importe qui.
Comme Ansible donc, il se base sur SSH. Il n’y a donc aucun agent particulier à installer. L’authentification est gérée par SSH, on peut donc se logger par login/mot de passe ou par clé SSH. Sudo est géré pour la délégation de droits ainsi que les primitives de transfert de fichiers.

2 On en est à quelle version déjà ?

Fabric première version est la version historique, disponible dans les dépôts ubuntu 14.04 et 16.04 qui a comme grand défaut de n’être compatible que jusqu’à Python 2.7 qui n’est plus supporté par la fondation Python depuis un an mais peut être encore par votre distribution.
Fabric2 est une réécriture majeure qui supporte Python3 dont le but avoué des développeurs est d’être plus facilement maintenable. Cependant, cette nouvelle version apporte une syntaxe largement modifiée et dont il manque encore de nombreuses fonctionnalités pour un sysadmin comme les groupes de machine. Mais pour un développeur qui cherche une surcouche à plus haut niveau que le shell pour déployer une application ou qui a besoin d’une intégration SSH avec Django au dessus de ce que fourni le framework paramiko, le produit est tout à fait adapté.
Enfin, ma version de prédilection est Fabric3. C’est un fork amical de Fabric1 conçu pour fonctionner avec Python > 3.4. L’installation passe par le système de packages Python pip, tout ce qu’il y a de plus classique :

pip3 install Fabric3

3 Tout commence par un fabfile

Le lancement de fabric se fait par la commande fab qui cherche ses instructions dans un fichier fabfile.py dans le répertoire courant. Malgré l’extension du fichier, les connaissances requis en Python sont très faibles pour écrire un fabfile.
Voyons donc un premier fabfile avec une seule fonction :
from fabric.api import abort, cd, env, get, hide, hosts, local, prompt, \
put, require, roles, run, runs_once, settings, show, sudo, warn, open_shell

def host_details():
run('lsb_release -a')

Et allons exécuter notre fonction sur l’un de nos serveurs :

fab -H ldap1 host_details

[ldap1] Executing task 'host_details'
[ldap1] run: lsb_release -a
[ldap1] Login password for 'julien':
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal
[ldap1] out:

Quand il s’agit de simplement lancer une commande à usage unique, il est possible d’appeler directement fabric avec en paramètre notre commande sans avoir besoin de modifier le fabfile.

fab -H ldap2,ldap1 -- lsb_release -a
[ldap2] Executing task ''
[ldap2] run: lsb_release -a
[ldap2] Login password for 'julien':
[ldap2] out: No LSB modules are available.
[ldap2] out: Distributor ID: Ubuntu
[ldap2] out: Description: Ubuntu 18.04.4 LTS
[ldap2] out: Release: 18.04
[ldap2] out: Codename: bionic

[ldap1] Executing task ''
[ldap1] run: lsb_release -a
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal

4 On mutualise avec les rôles

Je parlais d’administration d’un parc de serveur donc ma commande fabric elle est jolie, elle me permet de spécifier plusieurs serveurs en utilisant fab -H ldap1,ldap2 mais ce n’est pas très pratique à utiliser. Et puis surtout je n’ai pas besoin de connaître et saisir le nom de tous les serveurs lorsque je déploie une nouvelle configuration SNMP, en particulier lorsque le parc est mouvant.
Pour cela, Fabric expose une variable d’environnements env.roledefs, en pratique un dictionnaire permettant d’organiser nos serveurs par rôles. Bien entendu, un serveur peut se retrouver dans plusieurs rôles mais il n’est pas possible d’utiliser nos définitions de rôles sous la forme imbriquée, d’union ou d’intersection.

env.roledefs = {
'dns': ['ns1', 'ns2'],
'ldap': ['ldap1', 'ldap2'],
'world': ['ldap1', 'ldap2', 'ns1', 'ns2'],
}

Comme pour les hôtes, on peut au besoin lancer une commande sur plusieurs rôles en les séparant par une virgule. Et quand le nombre de machines devient trop grand, il est même possible de paralléliser l’exécution avec -P.

fab -R ldap host_details

[ldap1] Executing task 'host_details'
[ldap1] run: lsb_release -a
[ldap1] Login password for 'julien':
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal
[ldap1] out:
[ldap2] Executing task 'host_details'
[ldap2] run: lsb_release -a
[ldap2] out: No LSB modules are available.
[ldap2] out: Distributor ID: Ubuntu
[ldap2] out: Description: Ubuntu 18.04.4 LTS
[ldap2] out: Release: 18.04
[ldap2] out: Codename: bionic

Personnellement, j’ai tendance à découper mes rôles par profil de serveur (DB, LDAP, SMTP,etc…), par environnement (recette, production…) et créer un rôle qui contient l’ensemble des serveurs.

5 Tout SSH dans un fabfile

Pour le premier exemple nous avons simplement utilisé la primitive run mais fabric en dispose d’une multitude d’autres pour couvrir l’essentiel des cas d’usages d’un sysadmin :
– sudo : forcément lorsqu’on parle d’administration systèmes se pose la question des droits. Pas question de tout faire en root, d’ailleurs le compte est probablement désactivé à la connexion SSH. L’utilisation se fait de la même façon que la commande run.
sudo(‘systemctl restart snmpd.service’)
Et il est aussi possible de préciser l’utilisateur sous lequel exécuter la commande en sudo :

sudo('/opt/zimbra/bin/zmlocalconfig ldap_host',user="zimbra")

– Transférer des fichiers avec get et put :

put('snmpd.conf','/etc/snmp/snmpd.conf',use_sudo=True,mirror_local_mode=True)

ou à l’inverse :

get('/etc/resolv.conf','resolv.conf')

mirror_local_mode, optionnel, est bien pratique pour répliquer les permissions locales sur la machine distante. Il est tout autant possible de spécifier les permissions avec mode=’0640’ ou ne rien indiquer.
– Redémarrer un serveur : reboot(), par défaut avec un timer de 120 secondes.
– Interraction avec le sysadmin : prompt :

def installpkg():
  pkg = prompt('Lequel monseigneur?')
  sudo('apt -y install '+pkg)

– Les contextes : ils permettent de redéfinir une partie de l’environnement pour fournir un contexte d’exécutions au commandes qui seront lancées.

def run_commande():
  with shell_env(PATH='/opt/zimbra/bin:$PATH'):
  run("commande")

lcd modifie le chemin de la machine sur laquelle le fabfile est exécuté et cd la machine sur laquelle on se connecte.

def update_git() :
  with cd("/srv/project"):
  run("git pull")

6 Environnement

Fabric propose également certaines dispositions pour interragir avec les variables d’environnement. De manière absolument pas sécurisée, les variables env.user et env.password permettent d’indiquer les identifiants de connexion de manière globale dans le fabfile, la dernière chose à montrer à son RSSI donc.
Néanmoins, d’autres aspects de l’environnement ont toute leur place sur un grand parc. Notamment, si je veux collecter tous les fichiers /etc/resolv.conf de mes serveurs pour les auditer, je peux facilement les récupérer localement nommés de manière appropriée :
def get_resolvconf():
get(‘/etc/resolv.conf’,’/tmp/resolv.conf-+env.host)
8 Un exemple dans le monde réel
Pour ne pas finir sur une bête liste de commandes, voici ce que nous donnerait un script fabric pour déployer un service snmp avec un fichier de configuration local. Il y a de fortes chances que du snmp soit standardisé sur un parc de machines et l’utilisation d’un outil d’automatisation tel que Fabric a toute sa place.

from fabric.api import abort, cd, env, get, hide, hosts, local, prompt, \
put, require, roles, run, runs_once, settings, show, sudo, warn, open_shell

env.roledefs = {
'dns': ['ns1', 'ns2'],
'ldap': ['master', 'slave'],
'world': ['ldap1', 'ldap2', 'ns1', 'ns2'],
}

def yumcleanup():
  sudo('yum clean all')

def inst_snmp():
  sudo('yum install net-snmp')
  sudo('systemctl enable snmpd.service')

def conf_snmp():
  put('snmpd.conf','/etc/snmp/snmpd.conf',use_sudo=True,mode=’0600’)
  sudo('systemctl restart snmpd.service')

def deploy_snmp():
  yumcleanup()
  inst_snmp()
  conf_snmp()

Il suffit ensuite d’appeler la commande deploy_snmp pour déclencher la configuration du SNMP sur les serveurs désirés. Et en quelques lignes de Python de plus, il est possible de rendre les commandes neutres sur le système de packages utilisé par la distribution.

Conclusion

Fabric n’est plus tout jeune dans le monde Linux et OpenSource désormais. Bien que largement dépassé par d’autres solutions autour de SSH comme Salt ou Ansible sur le terrain des fonctionnalités, ces logiciels ne doivent pas être opposés.
Je considère qu’un outil simple garde son intérêt pour sa souplesse et sa facilité d’utilisation. Il est en effet bien plus simple d’écrire une fonction de quelques lignes à exécuter sur plusieurs machines que l’équivalent Ansible, surtout quand c’est du code jetable.

Gnome clic droit et touchpad

4 avril 2024 à 19:26

Dans certaines conditions en particulier avec les touchpad non synaptic le clic droit est désactivé par défaut.

Pour lister les options du périphérique de pointage :

julien@gollum:~$ gsettings list-recursively org.gnome.desktop.peripherals.touchpad
org.gnome.desktop.peripherals.touchpad accel-profile 'default'
org.gnome.desktop.peripherals.touchpad click-method 'areas'
org.gnome.desktop.peripherals.touchpad disable-while-typing true
org.gnome.desktop.peripherals.touchpad edge-scrolling-enabled false
org.gnome.desktop.peripherals.touchpad left-handed 'mouse'
org.gnome.desktop.peripherals.touchpad middle-click-emulation false
org.gnome.desktop.peripherals.touchpad natural-scroll true
org.gnome.desktop.peripherals.touchpad send-events 'enabled'
org.gnome.desktop.peripherals.touchpad speed 0.31506849315068486
org.gnome.desktop.peripherals.touchpad tap-and-drag true
org.gnome.desktop.peripherals.touchpad tap-and-drag-lock false
org.gnome.desktop.peripherals.touchpad tap-button-map 'default'
org.gnome.desktop.peripherals.touchpad tap-to-click true
org.gnome.desktop.peripherals.touchpad two-finger-scrolling-enabled true

Pour identifier le mode de clic actuel :

julien@gollum:~$ gsettings get org.gnome.desktop.peripherals.touchpad click-method
'fingers'

Pour lister les modes de clic disponibles :

julien@gollum:~$ gsettings range org.gnome.desktop.peripherals.touchpad click-method
enum
'default'
'none'
'areas'
'fingers'

Enfin, modifier la configuration :

julien@gollum:~$ gsettings set org.gnome.desktop.peripherals.touchpad click-method 'areas'

Docker swarm et NFS

19 décembre 2023 à 21:37

Docker Swarm fournit de manière native via son driver local, la possibilité de monter un système de fichiers partagé. En natif, nous allons retrouver NFS, Glusterfs ou SSHFS par exemple. Dans mon cas, ce sera NFS :

Mon cluster Swarm est le suivant :

julien@swarm0:~$ docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
uhr91g3ak37vhumtlpic86gwl *   swarm0     Ready     Drain          Leader           24.0.7
qmsjds24sx5ed9ez9q8quas99     swarm1     Ready     Active                          24.0.7
uxigsaj9j6z0y6hdsyxz162my     swarm2     Ready     Active                          24.0.7

Le volume partagé est construit depuis un serveur NFS en HA géré par mon rôle Ansible DRBD+Heartbeat.

Sur les worker node il est nécessaire de disposer du client nfs, disponible via le package nfs-common sous Debian/Ubuntu.

On peut ensuite passer à son fichier docker compose :

version: "3.2"

volumes:
  www-data:
    driver: local
    driver_opts:
      type: nfs
      o: "addr=192.168.69.30,rw"
      device: ":/mnt/nfs"

networks:
  nginx:
    driver: overlay

services:
  nginx:
    image: nginx:latest
    networks:
      - "nginx"
    ports:
      - "8080:80"
    volumes:
      - type: volume
        source: www-data
        target: /var/www/html
        volume:
          nocopy: true
    deploy:
      replicas: 2
      restart_policy:
        condition: any

Déployons la stack :

docker stack deploy --compose-file nginx.yml nginx-stack

Vérifions notre volume :

docker volume ls
DRIVER    VOLUME NAME
local     nginx-stack_www-data
docker volume inspect nginx-stack_www-data
[
    {
        "CreatedAt": "2023-12-19T21:30:41Z",
        "Driver": "local",
        "Labels": {
            "com.docker.stack.namespace": "nginx-stack"
        },
        "Mountpoint": "/var/lib/docker/volumes/nginx-stack_www-data/_data",
        "Name": "nginx-stack_www-data",
        "Options": {
            "device": ":/mnt/nfs",
            "o": "addr=192.168.69.30,rw",
            "type": "nfs"
        },
        "Scope": "local"
    }
]

Premiers pas avec Docker Swarm

21 mars 2023 à 21:14

Principe

Docker Swarm est un système d’orchestration de conteneur incorporé à Docker Engine. Moins populaire et complet de Kubernetes, il est aussi plus simple à utiliser.

Un cluster Swarm est composé de deux types de noeuds :

  • Les manager nodes qui gèrent la configuration du cluster
  • Les worker nodes qui offrent de la capacité de calcul

Swarm va permettre de déployer des services. Un service est l’état désiré d’un ou plusieurs conteneurs, les ports exposés, le nombre de conteneurs à instancier pour assurer le fonctionnement etc… Le manager node va ensuite se charger de gérer l’orchestration sur les worker afin d’offire le fonctionnement désiré.

Pour la suite, je disposerai de 3 nodes sous Ubuntu 22.04 :

  • swarm0 / 192.168.69.60 : manager node
  • swarm1 / 192.168.69.61 : worker node
  • swarm2 / 192.168.69.62 : worker node

Si docker-ce n’est pas installé, cela se règle rapidement avec :

sudo apt update
sudo apt -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
sudo apt -y remove docker.io
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg |apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt -y install docker-ce
sudo systemctl enable --now docker

Créer le cluster

Docker Swarm est compris par défaut donc il n’y a rien de plus à installer. On peut donc initier le cluster sur le manager avec en advertise address, l’ip du manager :

sudo docker swarm init --advertise-addr 192.168.69.60
Swarm initialized: current node (qeo9wfwnm22pd9mae9hscxfcs) is now a manager.

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

    docker swarm join --token SWMTKN-1-13bd0a7lozmekt5ph18igdtngwnqqe9s8lezwcxybqcz05p1gb-0dt1bomyoleflgzy58xbri5e6 192.168.69.60:2377

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

Le token est à conserver de manière sécurisée dans un vault ou un keepass par exemple.

On peut maintenant joindre les worker node au manager node avec ce toker et en précisant l’IP du manager :

sudo docker swarm join --token SWMTKN-1-13bd0a7lozmekt5ph18igdtngwnqqe9s8lezwcxybqcz05p1gb-0dt1bomyoleflgzy58xbri5e6 192.168.69.60:2377
[sudo] password for julien:
This node joined a swarm as a worker.

Vérifions nos noeuds :

sudo docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
qeo9wfwnm22pd9mae9hscxfcs *   swarm0     Ready     Active         Leader           23.0.1
aq0jchfcz0c0vedu6gsdteoav     swarm1     Ready     Active                          23.0.1
daspjlhzxpk2xkg9mpbf70s2l     swarm2     Ready     Active                          23.0.1

Lancer un service

Avec Swarm, on ne déploie pas un simple conteneur, il serait instancié bien entendu mais géré comme un conteneur autonome.
On va donc se créer un fichier docker-compose.yml et le déployer sur le cluster. Ici, on va simplement déployer une image nginx avec deux réplicas.

version: "3"
services:
  web:
    image: nginx
    deploy:
      replicas: 2
    ports:
     - "8080:80"
    environment:
     - NGINX_HOST=morot.local
     - NGINX_PORT=80

Et vérifier que le service est bien déployé

julien@swarm0:~$ sudo docker stack deploy --compose-file docker-compose.yml nginx-stack
Creating network nginx-stack_default
Creating service nginx-stack_web

Il est possible de visualiser où nos conteneurs sont lancés :

sudo docker service ps nginx-stack_web
ID             NAME                IMAGE          NODE      DESIRED STATE   CURRENT STATE           ERROR     PORTS
fhgipvoq55wl   nginx-stack_web.1   nginx:latest   swarm1    Running         Running 5 minutes ago
wd2obqai9xoj   nginx-stack_web.2   nginx:latest   swarm0    Running         Running 5 minutes ago

Sur chaque noeud on peut visualiser les conteneurs qui y sont lancés.

 sudo docker ps
[sudo] password for julien:
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS     NAMES
569eeae4351e   nginx:latest   "/docker-entrypoint.…"   6 minutes ago   Up 6 minutes   80/tcp    nginx-stack_web.1.fhgipvoq55wl6qfuals6cxpdg

Après arrêt d’un des deux worker, le conteneur lancé sur swarm1 est lancé sur swarm2 :

julien@swarm0:~$ sudo docker service ps nginx-stack_web
ID             NAME                    IMAGE          NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
w0ns4o93q4dp   nginx-stack_web.1       nginx:latest   swarm2    Running         Running 18 seconds ago
fhgipvoq55wl    \_ nginx-stack_web.1   nginx:latest   swarm1    Shutdown        Running 8 minutes ago
wd2obqai9xoj   nginx-stack_web.2       nginx:latest   swarm0    Running         Running 8 minutes ago

Maintenant, faisons en sorte que notre manager ne soit plus un worker node :

julien@swarm0:~$ docker node update --availability drain swarm0
swarm0

Pour assurer le nombre de réplicas, un second conteneur a été démarré sur le worker swarm 2 :

sudo docker service ps nginx-stack_web
ID             NAME                    IMAGE          NODE      DESIRED STATE   CURRENT STATE             ERROR     PORTS
w0ns4o93q4dp   nginx-stack_web.1       nginx:latest   swarm2    Running         Running 5 minutes ago
fhgipvoq55wl    \_ nginx-stack_web.1   nginx:latest   swarm1    Shutdown        Running 13 minutes ago
s8olvpwus904   nginx-stack_web.2       nginx:latest   swarm2    Running         Running 10 seconds ago
wd2obqai9xoj    \_ nginx-stack_web.2   nginx:latest   swarm0    Shutdown        Shutdown 12 seconds ago

Redémarrons le node précédemment arrêté et pour assurer la montée en charge, nous allons indiquer que nous souhaitons 3 réplicas désormais :

docker service scale nginx-stack_web=3
nginx-stack_web scaled to 3
overall progress: 3 out of 3 tasks
1/3: running   [==================================================>]
2/3: running   [==================================================>]
3/3: running   [==================================================>]
verify: Service converged

docker service ls
ID             NAME              MODE         REPLICAS   IMAGE          PORTS
llv38z9l6a0p   nginx-stack_web   replicated   3/3        nginx:latest   *:8080->80/tcp

sudo docker service ps nginx-stack_web
ID             NAME                    IMAGE          NODE      DESIRED STATE   CURRENT STATE                ERROR     PORTS
w0ns4o93q4dp   nginx-stack_web.1       nginx:latest   swarm2    Running         Running 12 minutes ago
fhgipvoq55wl    \_ nginx-stack_web.1   nginx:latest   swarm1    Shutdown        Shutdown 2 minutes ago
s8olvpwus904   nginx-stack_web.2       nginx:latest   swarm2    Running         Running 7 minutes ago
wd2obqai9xoj    \_ nginx-stack_web.2   nginx:latest   swarm0    Shutdown        Shutdown 7 minutes ago
k9g4x9kjsham   nginx-stack_web.3       nginx:latest   swarm1    Running         Running about a minute ago

Faisons le ménage et supprimons notre stack :

sudo docker stack rm nginx-stack
Removing service nginx-stack_web
Removing network nginx-stack_default

Stockage des sessions PHP dans memcached

4 mars 2023 à 20:41

Contexte :

Sans configuration particulière, les sesions PHP sont stockées dans un répertoire local sur le système de fichiers du serveur.
Cependant, dans le cas d’une infrastructure web composée de plusieurs serveurs derrière un load balancer, si l’utilisateur se trouve aiguillé sur un second serveur, il perd alors sa session.
Pour assurer une continuité du service, on peut utiliser un système de fichiers partagé ou un système en mémoire comme Memcached.

Memcached:

On installe le service sur chacun des frontaux web :

root@www1:~# apt -y install memcached
root@www1:~# systemctl enable memcached

On configure memcache pour à minima écouter sur le réseau et naturellement il faudra redémarrer memcached après cela :

root@www1:~# cat /etc/memcached.conf
-d
logfile /var/log/memcached.log
-m 64
-p 11211
-u memcache
-l 0.0.0.0
-P /var/run/memcached/memcached.pid

PHP :

Côté PHP, on aura besoin de l’extension correspondant à la version de PHP installée.
PHP fourni deux extensions, -memcache et -memcached, plus récente. C’est bien la première que nous voulons.
Exemple :

root@www1:~# apt -y install php8.1-memcache

Ensuite, côté PHP, il faut indiquer memcache comme session handler dans le fichier php.ini.
Côté Debian, celui-ci est géré par version et par type de module (cli,mod apache, ou php-fpm) dans le répertoire /etc/php/version/install_type.

Chaque serveur memcache doit être indiqué car il faut se rappeler que les différents serveurs memcache n’opèrent pas comme un vrai cluster.

session.save_handler = memcache
session.save_path = 'tcp://192.168.10.1:11211,tcp://192.168.10.2'

Enfin on va indiquer dans la configuration du module memcache (conf.d/20-memcache.ini sous Debian/Ubuntu) le mode de stockage des clés :

memcache.allow_failover=1
memcache.session_redundancy=3
memcache.hash_strategy=consistent

Le nombre de session_redundancy doit être égal au nombre de serveurs memcache + 1.

❌