Vue lecture
Gérez votre stockage avec Stratis (Publié dans Linux Pratique N°128)
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)
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
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
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
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
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.
Utilisez Terraform pour vos projets Docker (Publié dans GLMF 240)
Terraform est un outil populaire pour déployer de l’infrastructure en particulier à destination des Cloud Publics. Cependant, il possède de nombreux providers pour dialoguer avec différents hyperviseurs, bases de données ou solutions d’infrastructures en Software Defined. Voyons dans cet article son utilisation avec Docker.
/// Titre Couv = Outils / Infrastructure as Code #7639 : Utilisez Terraform avec vos projets Docker ///
Qu’est-ce que l’Infrastructure As Code ?
Le concept de l’Infrastructure as Code a pour but d’automatiser le déploiement des systèmes et infrastructures par l’utilisation de méthodes qui se rapprochent du développement logiciel comme l’utilisation de scripts ou de fichiers de configuration. Ainsi, le processus de déploiement se trouve plus rapide, sans risque d’erreur humaine, répétable et au final mieux maîtrisé.
Les outils de type Puppet ou Ansible sont capables dans un certaine mesure d’interagir avec une couche infrastructure, toutefois leur rôle est privilégié pour ce qui est de la configuration des systèmes. À l’inverse, Terraform ne se préoccupe pas des systèmes d’exploitation mais agit comme un formidable orchestrateur vers différentes solutions à partir du moment où elles disposent d’une API. Leur utilisation conjointe est donc tout à fait possible si ce n’est recommandé.
Dans le cadre d’une utilisation avec Docker, Terraform ne se préoccupe pas de la gestion des images. Leur création avec un DockerFile et leur publication sur une registry restent d’actualité et ceci est géré en dehors de Terraform. Son rôle démarre au moment de l’instanciation des conteneurs depuis une image. Hashicorp, l’éditeur derrière Terraform dispose toutefois d’un outil pour la gestion des images dont la logique est similaire : Packer.
1 Instancier un premier conteneur
1.1 Pré-requis
Si le runtime Docker n’est pas présent sur votre machine, l’installation est très simple. Pour une CentOS ou une RedHat, il faudra en plus ajouter le dépôt Docker-ce à votre système.
$ sudo apt install docker.io $ sudo systemctl enable docker.service $ sudo systemctl start docker.service $ sudo usermod -aG docker $USER
Terraform est distribué sous la forme d’un unique binaire compilé en statique, par conséquent il suffit d’extraire le contenu du zip téléchargé sur le site de l’éditeur et de le placer dans un répertoire de préférence présent dans votre ${PATH}.
$ wget https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip $ sudo unzip -d /usr/local/bin/ terraform_0.12.24_linux_amd64.zip Archive: terraform_0.12.24_linux_amd64.zip inflating: /usr/local/bin/terraform
Voici, c’est toute la simplicité et l’élégance de Terraform. Il n’y a rien de plus à déployer ou configurer pour rendre l’outil fonctionnel. On peut d’ores et déjà écrire nos premières lignes de code.
1.2 Un conteneur en quelques lignes
Nous allons dans un premier temps instancier un conteneur avec la dernière image Ubuntu via un fichier Terraform. En ligne de commande, ce sera l’équivalent de ces deux commandes :
$ docker pull ubuntu:latest $ docker run -d --name terraform_container ubuntu:latest
Il n’y a pas d’impératifs forts pour l’organisation des fichiers Terraform. Cependant, la convention veut que le code principal se trouve dans un fichier nommé main.tf.
Tout démarre par la déclaration du provider, en l’occurrence Docker dans notre cas avec la méthode de connexion. Dans le cas le plus simple, il s’agit d’utiliser la socket unix locale, il faut donc avoir les droits sur la socket. Dans le cadre d’une utilisation à distance, avec SSH par exemple, cela amène donc à la nécessité de déclarer les credentials à utiliser pour s’authentifier. Toujours dans notre fichier main.tf, on déclare ensuite les ressources que nous souhaitons instancier. Pour un conteneur nommé terraform_container depuis la dernière image Ubuntu, cela donne :
provider "docker" { host = "unix:///var/run/docker.sock" } resource "docker_image" "ubuntu" { name = "ubuntu:latest" } resource "docker_container" "terraform_container" { image = docker_image.ubuntu.latest name = "terraform_container" }
1.3 Cycle de vie
Le conteneur est décrit dans un exemple de code mais n’est pas encore instancié tant que l’on n’a pas fait appel à Terraform. Nous allons dans un premier temps avoir besoin du provider Terraform pour Docker [1], le binaire que nous avons téléchargé n’en disposant pas. Terraform va automatiquement détecter le backend dont il a besoin depuis le code :
$ terraform init Initializing the backend... Initializing provider plugins... - Checking for available provider plugins... - Downloading plugin for provider "docker" (terraform-providers/docker) 2.7.0... [...] * provider.docker: version = "~> 2.7" Terraform has been successfully initialized!
Nous allons maintenant générer un « plan ». Le plan ne crée aucun changement sur l’infrastructure mais va analyser les actions qui doivent être entreprises pour appliquer les changements souhaités sur l’infrastructure. Cela permet de visualiser les actions que Terraform souhaite entreprendre avant qu’elles ne soient appliquées, en particulier lorsque ces changements sont sensibles. Optionnellement, le plan peut être sauvegardé dans un fichier.
$ terraform plan Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. docker_image.ubuntu: Refreshing state... [id=sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bdubuntu:latest] ------------------------------------------------------------------------ An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # docker_container.terraform_container will be created + resource "docker_container" "terraform_container" { + attach = false + bridge = (known after apply) + command = (known after apply) + container_logs = (known after apply) + entrypoint = (known after apply) + env = (known after apply) + exit_code = (known after apply) + gateway = (known after apply) + hostname = (known after apply) + id = (known after apply) + image = "sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bd" + ip_address = (known after apply) + ip_prefix_length = (known after apply) + ipc_mode = (known after apply) + log_driver = "json-file" + logs = false + must_run = true + name = "terraform_container" + network_data = (known after apply) + read_only = false + restart = "no" + rm = false + shm_size = (known after apply) + start = true + labels { + label = (known after apply) + value = (known after apply) } } Plan: 1 to add, 0 to change, 0 to destroy. [...]
Si la revue du plan est conforme aux actions souhaitées, il peut être appliqué :
$ terraform apply docker_image.ubuntu: Refreshing state... [id=sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bdubuntu:latest] [...] Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? [...] docker_container.terraform_container: Creating... docker_container.terraform_container: Creation complete after 0s [id=2e6dc1f72de2ba701bbebb355d23515ff3380ff698d4dc39150bc8b8c4686ba3] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Vérifions que notre conteneur est bien instancié :
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 133caea16805 4e5021d210f6 "/bin/bash" 12 minutes ago Exited (0) 5 seconds ago terraform_container
C’est parfait, Terraform a fait le job. À l’issue de son exécution, un fichier tfstate a été généré afin de pouvoir conserver un lien entre ce qui a été généré et le code de l’infrastructure. Cela permet également de gérer l’ensemble du cycle de vie du provisionning de l’infrastructure, ce que nous allons faire en décommissionnant notre conteneur :
$ terraform destroy docker_image.ubuntu: Refreshing state... [id=sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bdubuntu:latest] docker_container.terraform_container: Refreshing state... [id=133caea16805a38a3997f7a2fad438e1030a6c3a09ec732db76e706dc22ec217] An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: - destroy Terraform will perform the following actions: # docker_image.ubuntu will be destroyed - resource "docker_image" "ubuntu" { - id = "sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bdubuntu:latest" -> null - latest = "sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bd" -> null - name = "ubuntu:latest" -> null } Plan: 0 to add, 0 to change, 1 to destroy. [...]
Que ce soit utilisé avec Docker, VMWare ou AWS [2], cette logique d’utilisation reste dans tous les cas valable.
2 Terraform dans le monde réel
L’exemple précédent n’était pas pertinent dans le périmètre d’un unique conteneur mais permettait de s’approprier Terraform. Dès qu’il est question d’avoir plusieurs conteneurs qui doivent communiquer, d’exposer une application sur le réseau ou de permettre à un conteneur de disposer d’un stockage persistant le besoin est tout autre.
L’outil de prédilection avec Docker est plutôt Docker Compose mais d’une part l’utilisation du YAML rebute certaines personnes et d’autre part son utilisation est limitée à ce système de conteneurisation. Terraform propose une autre syntaxe pour arriver à ses fins.
Pour que le lecteur puisse facilement comparer, je suis parti de la stack Docker WordPress sur le dockerhub [3] et je vous propose une version écrite en Terraform. Cette stack déploie un conteneur de base de données et deux volumes persistants. J’ai juste remplacé MySQL par MariaDB pour ne pas déployer un produit issu d’Oracle sur ma machine.
Voici donc ce que donne notre stack WordPress réécrite en Terraform :
provider "docker" { host = "unix:///var/run/docker.sock" } resource "docker_network" "private_network" { name = "wp_net" } resource "docker_volume" "wp_vol_db" { name = "wp_vol_db" } resource "docker_volume" "wp_vol_html" { name = "wp_vol_html" } resource "docker_container" "db" { name = "db" image = "mariadb" restart = "always" network_mode = "wp_net" mounts { type = "volume" target = "/var/lib/mysql" source = "wp_vol_db" } env = [ "MYSQL_ROOT_PASSWORD=rootpassword", "MYSQL_DATABASE=wordpress", "MYSQL_USER=exampleuser", "MYSQL_PASSWORD=examplepass" ] } resource "docker_container" "wordpress" { name = "wordpress" image = "wordpress:latest" restart = "always" network_mode = "wp_net" env = [ "WORDPRESS_DB_HOST=db", "WORDPRESS_DB_USER=exampleuser", "WORDPRESS_DB_PASSWORD=examplepass", "WORDPRESS_DB_NAME=wordpress" ] ports { internal = "80" external = "8080" } mounts { type = "volume" target = "/var/www/html" source = "wp_vol_html" } }
Un terraform apply plus loin, nous pouvons vérifier le déploiement de nos deux conteneurs et accéder à notre application sur le port 8080 :
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d52385968da1 mariadb "docker-entrypoint.s…" 11 seconds ago Up 10 seconds 3306/tcp db 58b40a7d5d41 wordpress:latest "docker-entrypoint.s…" 22 seconds ago Up 21 seconds 0.0.0.0:8080->80/tcp wordpress
Conclusion
La grande force de Terraform à mon sens est de ne pas jouer le rôle d’une couche d’abstraction aux différents providers. C’est souvent un facteur de bugs ou une limitation sur un dénominateur commun de fonctionnalités, qui s’en trouvent forcément réduites. En effet, les ressources manipulées sont directement celles de la solution administrée. On ne manipule pas une VM mais une instance EC2 ou une machine virtuelle VM avec leurs spécificités et fonctionnalités, ne limitant donc pas l’éventail de possibilités à un sous-ensemble de fonctionnalités partagées.
Le grand intérêt de Terraform réside dans la possibilité de pouvoir utiliser un même outil et un même flux de travail avec une grande variété de providers.
Références
[1] Provider Docker pour Terraform : https://www.terraform.io/docs/providers/docker/index.html
[2] J. MOROT, « Infrastructure As Code sous AWS avec Terraform », GNU/Linux Magazine n°216, juin 2018 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-216/Infrastructure-As-Code-sous-AWS-avec-Terraform
[3] Image WordPress sur DockerHub : https://hub.docker.com/_/wordpress/