1. Cas d'école
Mettons nous en situation: je suis un administrateur système, je possède un parc de 200 machines que je loue à des clients qui, pour leur écrasante majorité, développent des applications PHP qui possèdent evidemment chacune leur spécificité et besoins propres, Drupal, Wordpress, Magento, Joomla, developpement custom, impossible de prédire avec précision quel sera le module sorti de derrière les fagots que notre client souhaitera installer (et configurer) sur son serveur. Il veut son mod_machin qui embarque 2000 dépendances, et comme il va stocker moult contenu à haute teneur multimedia, il veut également un point de montage sur votre SAN prévu à cet effet, il veut également pouvoir se connecter uniquement par le biais d'un VPN pour lequel son serveur sera client et pas serveur, et j'en passe et des meilleures. Pour corser la chose, ces machines sont en production (cri d'horreur), ainsi, point de place à l'amateurisme, aux redémarrages ou autres bidouilles experimentales. Non, nous avons besoin d'une sorte de chef d'orchestre tout puissant, souple mais pas trop, qui nous permette en un coup d'automatiser des actions proprement.
Lorsqu'on démarre une activité, et en particulier une activité d'hébergement, les clients ne se bousculent pas, et on a tout le temps nécessaire pour choyer chaque serveur, mais tout le mal qu'on peut se souhaiter, c'est que cette activité se développe, et possiblement de façon exponentielle, et là, c'est le drame, et l'ont s'endort en se répétant “pourquoi je n'ai pas industrialisé, POURQUOI ??”. Car en effet, pour citer mon ami stephbul, “il faut perdre du temps pour gagner du temps”, autre tournure de la phrase bien connue “les admins c'est qu'un tas de feignasses”. Effectivement, et c'est très bien comme ça.
Figure 1 : Des machines, des milllliooooooooons de machines (Equinix PA3, Saint-Denis)
Ceux d'entre vous ayant lu les excellents articles sur puppet parus dans ces mêmes colonnes pensent peut-être détenir le saint Graal de l'administration, et je le pensais également. Car si puppet est un excellent outil pour maintenir un parc statique, ou disons peu mouvant, c'est une toute autre histoire pour une infrastructure d'hébergement: le client est roi, et en tant que tel, ses volontés changent chaque matin ensoleillé. Ainsi, voici les points que nous avons identifiés comme étant prérequis à l'administration de masse, dans un parc hétérogène :
- pusher des informations
- segmenter notre infrastructure en “rôles”
- ne pas utiliser de protocole particulier
- rien de particulier à installer sur les serveurs à administrer
- permettre de scripter des ajouts et modifications
- récupération des codes de retour pour post-traitement
- possibilité d'étendre l'outil de base, possiblement à l'aide d'un langage connu
- gestion fine des droits utilisateur, en particulier la possibilité de sudo afin de ne pas avoir de privilèges lorsque cela n'est pas nécessaire
Munis de ces besoins, nous avons sillonné l'intarwebz à la recherche de la perle rare, et si plusieurs candidats ont retenu notre attention, l'un d'entre eux, de par son excellente documentation et sa facilité de mise en œuvre nous a séduit quasiment instantanemment : fabric.
2. Kenessé fabric ?
À l'heure où j'ecris ces lignes (Bonne Année à tous !), la version “stable” de fabric est la 0.9.3, et la version 1.0 est en cours de développement. De nombreux blogs et autres témoignages font état de la stabilité de la solution, et votre serviteur apporte sa pierre à l'édifice.
on peut lire ceci sur la page de garde du projet[1] : (traduction libre)
Fabric est une bibliothèque et un outil en ligne de commande Python ayant pour objectif de rationaliser l'utilisation de SSH pour le
déploiement d'applications ou les taches d'administration système.
Il fournit une suite d'opérations basiques permettant d’exécuter des commandes shell locales ou distantes (normalement par le biais de sudo)
et d'uploader/downloader des fichiers, ainsi que des fonctions auxiliaires qui demanderont une interaction utilisateur ou arrêteront
l’exécution.
L'utilisation classique implique la création d'un module Python qui contiendra une ou plusieurs fonctions, puis l'execution de ce module
par le biais de la commande fab.
Je n'aurais pas mieux dit.
Que ceux d'entre vous que l'apprentissage de Python rebute se rassurent, aucune notion de programmation n'est réellement nécessaire à la mise en place d'un système de déploiement fabric, car s'il serait évidemment plus élégant de réaliser certaines opérations par le biais du langage, rien ne nous empêche d'invoquer une commande shell pour ce faire. Mais nous verrons plus loin un exemple d'un tel cas de figure.
Démarrons notre apprentissage à l'aide d'un exemple simple; comme nous le suggère la page d'accueil du projet fabric, écrivons un module Python simplissime, aussi appelé fabfile :
$ cat fabtest.py
from fabric.api import run
def uptime():
run('uptime')
Figure 2 : Et si on vous upgradait toutes d'un coup, hein ?
Ici, nous importons de l'API fabric la seule méthode run, qui comme son nom l'indique, sert à executer une commande. Notez que l'execution de cette commande sera réalisée à l'aide de l'utilisateur qui appelle fabric si rien d'autre n'est spécifié. Nous reviendrons sur ce point plus tard. Nous déclarons ensuite une fonction à l'aide du mot clé def, cette dernière ne prend pas d'arguments. Enfin, nous appelons run avec pour seul argument la commande à executer, ici uptime. Il convient d'appeler ce module de cette façon :
$ fab -f fabtest.py -H tatooine,naboo uptime
La commande qui actionne tout le mécanisme fabric est fab. À cette commande, nous passons en argument :
- le fabfile à interpréter, c'est à dire le module Python qui contient les fonctions à prendre en compte
- les rôles ou bien le(s) nom(s) d'hôte(s) sur le(s)quel(s) nous allons executer notre fonction
- la fonction à executer
Important: nombre de fois, vous oublierez que c'est sur une machine distante que vous executez vos fonctions, n'oubliez pas que les commandes shell que vous invoquerez devront être présentes sur la cible ! Voici le résultat de notre invocation :
$ fab -f fabtest.py -H tatooine,naboo uptime
[naboo] run: uptime
[naboo] out: 13:01:10 up 54 days, 1:10, 2 users, load average: 0.00, 0.00, 0.00
[tatooine] run: uptime
[tatooine] out: 13:06:48 up 13:36, 6 users, load average: 0.12, 0.10, 0.09
Done.
Disconnecting from tatooine... done.
Disconnecting from naboo... done.
Et soudain, tout un monde de paresse s'ouvre à vous…
3. In portability we trust
Un parc est rarement homogène, que ce soit par simple envie de diversifier ses plateformes, par pur prosélytisme ou par la demande expresse d'un client, il est pas rare d'héberger d'autres Unices que GNU/Linux. En l'occurrence, notre parc est également constitué de Dead^WOpensolaris et, je vous le donne en mille, NetBSD. Voici le résultat de l'execution de notre dernier module sur une cible NetBSD :
[coruscant] run: uptime
[coruscant] err: bash: /bin/bash: No such file or directory
Fatal error: run() encountered an error (return code 127) while executing 'uptime'
Aborting.
Disconnecting from coruscant... done.
Déception et énervement, NON Bash n'est pas présent par défaut sur tous les Unices du monde, et OUI on peut vouloir scripter de gentils outils à l'aide d'autres shells. Fort heureusement, les auteurs de fabric, s'ils ont renseigné des valeurs par défaut pour l'ensemble des paramètres impliqués dans l’exécution à distance, ont prévu d'étendre ces valeurs de plusieurs manières. La première et la plus simple consiste à renseigner un fichier de configuration, ~/.fabricrc :
shell = /bin/sh -c
Surcharge à l'issue de laquelle nous pouvons constater le bon fonctionnement de notre commande :
$ fab -f fabtest.py -H coruscant uptime
[coruscant] run: uptime
[coruscant] out: 4:10PM up 67 days, 20:39, 1 user, load averages: 0.00, 0.00, 0.00
Done.
Disconnecting from coruscant... done.
Cependant, si cette méthode élégante est tout à fait viable dans le cadre d'une machine centrale d'administration, il peut être tout à fait fastidieux de de notifier tous ses petits camarades de changer leur ~/.fabricrc tous les quatre matins. Afin de rendre cette opération plus industrielle, il convient de surcharger les valeurs de configuration initiales dans le fichier fabfile contenant nos differentes fonctions. En réalité, lorsque la commande fab est invoquée, elle surcharge les valeurs du module env de fabric avec celles présentes dans le fichier ~/.fabricrc. Ainsi, lorsqu'on ajoute :
user = pinpin
C'est en réalité la valeur de env.user qui vaut désormais “pinpin”. Afin de réaliser cette opération dans le code, il suffit simplement d'ajouter l'import du module env et de spécifier la valeur du paramêtre dans notre fabfile :
from fabric.api import run,env
env.shell = "/bin/sh -c"
env.user = "pinpin"
def uptime():
run('uptime')
Et de constater :
[coruscant] run: uptime
[coruscant] out: 4:23PM up 67 days, 20:51, 1 user, load averages: 0.04, 0.01, 0.00
Done.
Disconnecting from coruscant... done.
L'ensemble de ces variables d'environnement est bien évidemment documenté sur le site de fabric.
4. MOAR SERVARS !1!
On est mignons avec nos trois serveurs, mais nous parlions précédemment de centaines de machines. Voici à mon sens un must-have de tout système de déploiement digne de ce nom : les rôles. Fabric n'est evidemment pas le seul à embarquer ce type de notion, notons simplement sa grande souplesse et sa facilité d'utilisation. Les rôles se déclarent au niveau des variables d'environnement, tout comme nous l'avons vu précedemment pour user et shell, mais il s'agit ici non plus d'une variable simple mais d'un dictionnaire. Exemple :
env.roledefs = {
'netbsd': ['coruscant', 'exar'],
'linux': ['tatooine', 'naboo'],
'solaris': ['temple']
}
Grâce à cette notion de rôles, nous pouvons regrouper sous un label l'ensemble des machines correspondant à une propriété particulière. L'exploitation des rôles en ligne de commande est extrêmement simple :
$ fab -f fabtest.py -R netbsd uptime
[exar] run: uptime
[exar] out: 5:02PM up 24 days, 6:37, 0 users, load averages: 0.00, 0.00, 0.00
[coruscant] run: uptime
[coruscant] out: 4:40PM up 67 days, 21:08, 1 user, load averages: 0.00, 0.00, 0.00
Done.
Disconnecting from exar... done.
Disconnecting from coruscant... done.
Bien évidemment, il est parfaitement possible de regrouper des machines identiques sous differents rôles, nous pourrions par exemple avoir :
env.roledefs = {
'netbsd': ['coruscant', 'exar'],
'linux': ['tatooine', 'naboo'],
'solaris': ['temple'],
'www': ['tatooine', 'exar', 'temple'],
'datas': ['tatooine','temple']
}
De manière à découper les rôles fonction des actions à mener. Pour notre part, nous avons découpé nos rôles fonction de leur emplacement physique (datacenter), système d'exploitation, service (web, base de données, serveur SMTP…), client ainsi qu'une cible 'all' qui agit sur l'intégralité des machines du parc.
5. Une brève histoire de contexte
Autre particularité de fabric, les “Context Managers” permettent d'appliquer des propriétés particulières à un contexte d’exécution. Ces contextes se définissent à l'aide de la directive with et sont au nombre de 4 :
- cd permet de se déplacer dans un répertoire précis avant de poursuivre les opérations. Exemple :
with cd("cd /home/pinpin"):
run("ls")
- hide cachera les sorties souhaitées, par exemple, si l'on le désire plus voir les messages de fabric concernant les opérations en cours d'execution, on spécifiera :
with hide("running"):
- À l'inverse, show permettra de spécifier que l'on souhaite explicitement voir un type de message. Par exemple, pour afficher les messages de debug, désactivés par défaut, on inscrira :
with show("debug"):
- setting permet quand à lui de spécifier des paramètres particuliers au sein d'un contexte, et ainsi bypasser les variables d'environnement. Par exemple :
with settings(user="pinpin"):
6. Fabric, un goût de paradis
Pour aller un peu plus loin dans la découverte de ce fabuleux moteur à flemme, je vous propose un petit tour d'horizon de quelques modules fort pratiques inclus dans les bibliothèques api, constituant les fonctions de base de fabric, et contrib, utilitaires tiers mais rapidement indispensables. Pour utiliser ces fonctionnalités, il vous faudra les importer, le plus simple consistant à importer tout le contenu des classes api et contrib de cette façon :
from fabric.api import *
from fabric.contrib import *
- sudo: puisque nous parlons d'administration, il va sans dire que nombre des opérations que vous serez amenés à réaliser devront l'être avec un UID autre que le votre, et très souvent l'UID 0. Fabric fournit pour cela la fonction sudo. L'utilisation est la même que pour la fonction run, par exemple
sudo("aptitude update")
ou encore
sudo("echo plop >> /home/pinpin/plop.log", user="pinpin")
Il va de soi que votre utilisateur a des droits correctement configurés dans le fichier sudoers de la cible.
- put et get: autre opération classique, l'envoi ou la récupération de fichier sur ou depuis un serveur. Les fonctions put et get permettent de très simplement réaliser ces actions :
put("archives/backup.tar.gz", "/home/pinpin/nacasse/encoreunbackup.tar.gz", mode=0744) # notez que l'argument "mode" est optionnel
et
get("/home/pinpin/.bash_history", "logs/nacasse.log")
- append: quel administrateur n'a jamais eu besoin d'ajouter une option à un fichier de configuration sur un nombre important de machines ? je ne vous crois pas. Quoi qu'il en soit, la fonction append nous fait gagner un temps considérable, jugez plutot :
append("PermitRootLogin false", "/etc/ssh/sshd_config", use_sudo=True) # si l'on a pas besoin de l'UID 0, l'argument use_sudo est optionnel
- comment et uncomment: et si, et si, commenter ou décommender une ligne dans un fichier, fastidieuse tâche s'il en est :
comment("/etc/ssh/sshd_config", "^Subsystem\ sftp\ /", use_sudo=True) # meme remarque que précedemment
Ici, le second argument, comme vous l'aurez probablement constaté, est une expression régulière qui servira à déterminer quelle ligne vous souhaitez commenter ou décommenter.
- contains: permet de tester si un fichier contient une chaîne de caractères, on peut optionnellement indiquer si la correspondance doit être exacte grâce à l'argument Exact=True (positionné par défaut à False) et si l'on a besoin des droits root à l'aide de l'argument use_sudo=True
contains("/etc/ssh/sshd_config", "PermitEmptyPasswords")
- sed, comme son nom l'indique, est l'implémentation de la commande UNIX sed munie des arguments -r (expressions régulières étendues) et -i (génération d'un backup). On pourra ainsi substituer une chaîne de caractères de cette façon :
sed("/etc/ssh/sshd_config", "PermitRootLogin yes", "PermitRootLogin no", use_sudo=True)
Comme pour les exemples précédents, l'argument use_sudo est optionnel. Notez que le backup généré aura l'extension .bak si l'argument backup n'est pas spécifié.
Nous n'avons vu ici que quelques-unes des fonctions fournies par l'environnement fabric, l'ensemble d'entre elles est clairement listé et explicité sur le site principal du projet dans la section “Documentation, API documentation”.
7. Mais euh, tu vas pas nous laisser comme ça hein ?
Voici pour finir un petit exemple très simple de module permettant de mettre à jour vos machines. On s’enquiert en premier lieu du système d'exploitation sur lequel nous fonctionnons, puis fonction de celui ci, commandons une mise à jour de la base de données de paquets. Si l'option do_upgrade est placée à yes via la ligne de commande, nous éxecutons une mise à jour :
from fabric.api import run,sudo,env
from fabric.context_managers import *
env.shell = "/bin/sh -c"
env.roledefs = {
'netbsd': ['coruscant', 'exar'],
'linux': ['tatooine', 'naboo'],
'solaris': ['temple'],
'all': ['tatooine', 'naboo', 'coruscant', 'exar', 'temple']
}
def uptime():
with hide("running"):
run('uptime')
def update(do_upgrade="no"):
opsys = run("uname -s")
if opsys == "Linux":
sudo("aptitude update -q=2")
if do_upgrade == "yes":
sudo("aptitude upgrade")
if opsys == "NetBSD" or opsys == "SunOS":
sudo("pkgin update")
if do_upgrade == "yes":
sudo("pkgin upgrade")
Ce qui nous donne le rendu suivant :
$ fab -f fabtest.py -R linux,netbsd update
[naboo] run: uname -s
[naboo] out: Linux
[naboo] sudo: aptitude update -q=2
[naboo] out: Reading package lists...
[tatooine] run: uname -s
[tatooine] out: Linux
[tatooine] sudo: aptitude update -q=2
[tatooine] out: Reading package lists...
[exar] run: uname -s
[exar] out: NetBSD
[exar] sudo: pkgin update
[exar] out: database for ftp://ftp.fr.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0/All is up-to-date
[coruscant] run: uname -s
[coruscant] out: NetBSD
[coruscant] sudo: pkgin update
[coruscant] out: database for ftp://ftp.fr.netbsd.org/pub/pkgsrc/packages/NetBSD/amd64/5.0/All is up-to-date
Done.
Disconnecting from tatooine... done.
Disconnecting from exar... done.
Disconnecting from coruscant... done.
Disconnecting from naboo... done.
Pour commander une mise à jour, nous aurions exécuté la commande suivante :
$ fab -f fabtest.py -R linux,netbsd update:do_upgrade=yes
Car, oui, il est également possible de passer des arguments aux fonctions fabric par le biais de la ligne de commande.
Comme nous le précisions dans la première partie de cet article, un néophyte de Python pourra sans peine manipuler fabric et mettre au point ses déploiements simplement à l'aide des fonctions mises à disposition par l'API. Cependant, avec un investissement minimal dans ce langage d'une grande simplicité, on sera très rapidement en mesure d'effectuer manipulations et calculs bien plus rapidement. Pensez-y, s'il est tout à fait aisé et réalisable de se fendre d'un grep|sort|uniq|sed, ne serait-il pas plus élégant de tirer parti des fonctions de manipulation de chaîne du langage lorsque celles-ci sont réellement abordables [2] ?
8. Les oiseaux se cachent pour mourir
Au sein de l'entreprise dans laquelle je travaille[3], la découverte de cet outil a considérablement modifié nos méthodes de déploiement et d'administration, car si les divers systèmes de bootstrapping de nos différents Unices (preseed Debian, kickstart RedHat, jumpstart Solaris…) simplifient l'installation initiale, ils n'adressent pas la capillarité d'un parc hébergeant pratiquement autant de machines que de clients. La possibilité de manipuler 200 machines en une commande, et par le biais d'un système maintenu, fiable et ouvert, donne une toute autre envergure au métier d'administrateur. À l'époque du Cloud Computing, donc des machines virtuelles jetables, où un client est en mesure de vous demander une augmentation de plateforme de 200% du jour au lendemain, cette approche est non seulement salvatrice, mais aussi gratifiante. L'administration système prend un nouveau tournant, on manipule son parc comme un programme informatique, des unités de calcul virtuelles, et cet art prend des tournures macroscopique.
Liens
[1] http://docs.fabfile.org/0.9.3/
[2] http://docs.python.org/library/string.html