L'idempotence ? Mais qu'est-ce que c'est quoi ça ?

L'idempotence est une propriété d'un traitement mathématique ou informatique qui donne le même résultat (état final) quel que soit le nombre de fois où il est appliqué à un état de départ. En mathématique, par exemple, les fonctions suivantes sont idempotentes :

  • f(x)=x+0
  • f(x)=x*1
  • f(x)=x*0

f(x) est donc une fonction idempotente si f(x)=f(f(x))=f(f(f(x)))=...

En informatique, un traitement appliqué à un état de départ et aboutissant à un état final donne toujours le même résultat final s'il est appliqué plusieurs fois. Ainsi, si la condition d'idempotence est respectée, un script de déploiement (d'un système, d'une configuration, d'une application) pourra être lancé plusieurs fois de suite (pour vérifier qu'il marche vraiment bien, parce qu'on a ajouté le truc oublié et qu'on ne veut pas repartir de zéro, ou simplement parce que le mec chargé de lancer le script est un boulet et a oublié qu'il vient de le faire 3 minutes avant).

Un traitement informatique peut être idempotent s'il respecte un de ces 3 principes :

  • il vérifie au début de son exécution si il a déjà été appliqué et ne fait rien dans ce cas
  • il commence par faire l'opération inverse de ce qu'il va finalement faire (par exemple supprimer les lignes de conf si elles sont présentes pour les ajouter ensuite)
  • il n'utilise par design que des fonctions, commandes, sous-ensembles eux-mêmes idempotents suivant les 2 principes ci-dessus

À propos des déploiements automatisés

Je n'essaierai pas de vous convaincre ici de l'utilité d'automatiser les déploiements (agilité, reproductibilité, rapidité, ...), je considère ce point comme acquis. Cela nécessiterait un article à part entière.

Nous partons donc du principe que, à minima, vous automatisez/scriptez/utilisez un système de gestion de configuration/whatever pour vos déploiements. J'utiliserai ci-dessous plusieurs fois le terme de script de déploiement. Si le système que vous utilisez n'est pas un script à proprement parler mais un framework/application/ce que vous voulez, faites le parallèle, ça marche aussi.

Oui, mais quels déploiements ?

À vrai dire, peu importe. L'idéal, bien sûr, étant que tous les déploiements, à partir de la machine physique vierge jusqu'à l'application en production soient automatisés. C'est le fantasme du "chouette, je viens d'avoir une nouvelle machine, je branche, j'appuie sur le gros bouton déploiement et hop, dans 5 minutes, mon service est en prod".

Ainsi, on peut automatiser le déploiement de l'hyperviseur sur une machine physique, la création des machines virtuelles (l'enveloppe), le déploiement des OS dans les machines virtuelles, leur configuration, le déploiement des middlewares nécessaires au fonctionnement de l'application et leur configuration, le déploiement de l'application, etc...

Et bien sûr, on souhaite le faire non pas sur un seul et unique système, mais sur un système de systèmes (oui, des équipements réseaux, des frontaux, des bases de données, des cluster nosql, des serveurs de web services, ... tout ça en même temps pour déployer non pas une application, mais un service)

Un script de déploiement, c'est avant tout du code.

Nous devons donc le considérer comme du code et le traiter comme tel. Et donc appliquer les mêmes méthodes de développement que pour une application :

  • Gérer nos outils de déploiement dans un système de gestion de version (cvs pour les dinosaures, svn, git, bazaar, ...)
  • Faire des tests unitaires
  • Pourquoi pas de l'intégration continue, surtout si notre outil de déploiement est modulaire et réutilisable
  • Et on peut aussi y appliquer les méthodes agile (oui, bon, on est pas obligé non plus de faire des planning games et des stand-up meetings, je déteste faire une réunion debout, mais on peut se poser autour d'une bière pour disserter de l'étape suivante)
  • Surtout, on doit décrire les exigences. Et l'idempotente fait partie de ces exigences.

Pourquoi en avons-nous besoin ?

Les outils/scripts/... de déploiement se doivent d'être idempotents.

Cette affirmation très péremptoire provient de l'observation de plusieurs problématiques rencontrées dans la vraie vie. Pas celle des bisounours, celle des vrais gens. Et dans cette vie-là, vous trouvez :

  • des développeurs de scripts de déploiement qui oublient les sorties/affichages pour la personne qui exécutera le déploiement et des angoissés qui pour être certains que le déploiement a bien été fait le relanceront une seconde fois même si c'est écrit en rouge, taille 48, police comic sans dans la doc qu'il ne faut pas le faire
  • des corrections en live/de dernière minute du dernier morceau de déploiement, au moment du passage en prod et qu'on n'a pas envie de repartir de zéro en désinstallant tout et qu'on se contente de relancer globalement le déploiement (oui, c'est crade...)
  • des étourdis qui ne se souviennent plus après leur café qu'ils ont déjà lancé un déploiement avant de partir en pause et qui recommencent
  • ... complétez avec vos propres expériences ...

Note : oui, je sais, ce n'est pas forcément un humain qui doit lancer le déploiement, mais c'est pareil, l'humain peut aussi relancer le truc qui lance les déploiements, ou décider de le rejouer juste pour voir parce qu'il est... humain.

D'autre part, c'est agréable, lorsqu'on développe un programme, de pouvoir le lancer plein de fois pour le tester ou tester les nouveaux morceaux, et que comme on est excité de découvrir ce qu'on a fait de bien (ou pas), on ne veut pas perdre du temps à recréer les conditions initiales idéales pour relancer son truc.

Enfin (mais la liste n'est pas exhaustive), plus on aura eu de cas tordu, récupérés par notre fameuse idempotence, plus on aura un déploiement robuste et moins on aura de problème au moment du passage -en prod- à l'étape suivante.

Donc, les déploiements automatiques se doivent d'être idempotents dans leur ensemble, on doit pouvoir les lancer plusieurs fois de suite sans impact sur le résultat final.

Mais ils doivent aussi être idempotents dans leurs sous-parties, parfois jusqu'au niveau de la commande unitaire. Si par exemple je crée un script de déploiement d'une appli qui installe sur un OS

  • apache
  • PHP (remplacez par l'interpréteur que vous préférez)
  • les confs qui vont bien
  • l'appli

et qu'ensuite, disons dans une version N+1, j'ai besoin d'ajouter une base mysql, je veux pouvoir ajouter mon morceau de code qui déploie mysql et relancer le tout dans son ensemble. Je m'attends alors à ce que ma base de données soit déployée mais que le reste, qui était déjà installé ne soit pas cassé.

Dans la pratique

Dans la pratique, quelques opérations unitaires sur une machine ont l'air idempotentes. Par exemple l'installation d'un package :

aptitude update && aptitude install -y monpackage

Je peux lancer cette commande plusieurs fois, j'aurais toujours le même résultat final, le paquet sera installé.

Sûr ? Certain ? Y compris si entre les deux invocations il y a eu une mise à jour du package sur mon dépôt ?

Si ce n'est pas garanti, il vaudrait mieux le modifier légèrement pour le garantir quand c'est possible, ou connaitre (et communiquer) les limites de son script de déploiement.

D'autres commandes échouent simplement si le résultat attendu est déjà présent. Il suffit de gérer l'exception et le tour est joué. Pensez à useradd par exemple qui ne créera pas l'utilisateur s'il existe déjà.

Enfin, pour tout le reste, on peut coder soi-même une vérification d'existence du résultat attendu avant l'appel à la commande qui va produire ce résultat. Par exemple, je peux faire un ls avant de créer un fichier pour voir s'il n'existe pas déjà, ou chercher la ligne de conf que je veux ajouter pour savoir si c'est vraiment nécessaire, etc... et si ce test réussi, alors je n'ai plus rien à faire, je saute ce morceau.

C'est tout ? C'est aussi simple ? Ah bin oui, mais non car dans ce cas, on ne vérifie pas que le contenu est correct.

Dans mon exemple d'application LAMP, mon script de déploiement installe et configure apache. Et comme je veux garantir l'idempotence, j'ai ajouté avant l'installation d'apache une vérification, qu'il n'est pas déjà installé, sinon je saute cette partie.

Maintenant, que se passe-t-il si apache est installé, mais qu'il est tout cassé ? Par exemple, après un premier déploiement, j'ai fait un rm -rf /etc/apache2 (juste pour le fun, hein, personne ne ferait une chose pareille en vrai :-p ).

Mon script va vérifier si le paquet est installé, comme il est installé, il ne va rien faire, et mon déploiement ne va pas bien se passer du tout.

Vérifier partiellement l'existence du résultat final ne permet pas de garantir que nous avons TOUT le résultat final.

Humm, une solution pourrait être dans ce cas de prévoir dans notre script de déploiement une suppression, un retour à l'état initial "vierge" avant le déploiement.

Là encore, c'est trop simple, tout simplement parce que d'une part, défaire systématiquement pour refaire la même chose est inefficace, on ne veut pas pénaliser 99% des lancements pour gérer les 1% de cas tordus, d'autre part on risque de casser autre chose. Par exemple la modif qui a été appliquée en prod et non reportée dans le script de déploiement (qui a dit bien fait ?) ou la seconde appli installée sur le même serveur et qui n'est pas gérée par le même script de déploiement (qui a répété bien fait ?).

La vrai bonne réponse est que le développeur du script de déploiement doit s'adapter à l'environnement cible et user d'un savant mélange de ces 2 techniques (vérification d'exécution préalable ou désinstallation préalable) suivant le contexte. Il doit également poser lui-même ses conditions sur l'état initial que son script de déploiement peut accepter ou non.

Alors, maintenant que nos script de déploiements sont idempotents, c'est le bonheur... ou pas.

Pourquoi cela ne suffit pas

Repartons du besoin. Lorsqu'on crée un script de déploiement c'est pour obtenir comme résultat final un bouzin fonctionnel. Et on sait qu'il sera fonctionnel si tel et tel prérequis est respecté. Notre script de déploiement a donc pour mission de mettre en place ces prérequis. Nous cherchons donc, en plus de l'idempotence nécessaire, à obtenir un état final constant quel que soit l'état initial.

En d'autres termes, ce qu'on cherche, c'est que notre script de déploiement f, peu importe l'état initial x, donne toujours le résultat c. f(x)=c

Je ne connais pas le nom de cette propriété, malheureusement, mais s'il y a des plus matheux que moi dans le coin, n'hésitez pas à compléter en commentaire.

Cela tombe bien, parce que ce type de fonction est aussi idempotent.

Et là, je ne vois que 2 solutions pour le garantir :

  • Soit l'état initial est le vide absolu. Dans ce cas, comme on construit tout, on est certain de ce qu'on obtient. il faut donc partir de l'installation de l'OS (on supposera pour l'exercice qu'en dessous de l'OS, c'est le vide absolu. C'est un peu réducteur mais on est certain que l'OS vierge d'installation est reproductible aisément)
  • Soit nous sommes obligés d'imposer des prérequis, de poser des exigences sur l'état initial.

En conclusion

L'art noble et délicat du développement d'un script de déploiement doit respecter quelques principes :

  • Edicter et documenter les exigences ou prérequis à l'exécution de ce script (il est d'ailleurs mieux de les vérifier au lancement)
  • Être idempotent dans son ensemble
  • Être idempotent dans ses sous-parties

Enfin, et surtout, la qualité du déploiement utilisé dépend principalement de l'humain qui l'a codé, de sa maitrise technique, et de sa capacité à anticiper les cas problématiques plutôt qu'à les découvrir à l'utilisation.


Comments

comments powered by Disqus