Des patterns en Infra As Code ?

Introduction : L'Émergence et les Défis de l'Infrastructure as Code

Dans l'univers du développement logiciel, l'histoire a montré la nécessité d'adopter des cadres méthodologiques robustes pour bâtir des systèmes pérennes. Des principes tels que l'architecture en couches, l'architecture hexagonale (souvent appelée "Clean Architecture"), la séparation des responsabilités sont devenus des piliers. Ces approches ont collectivement permis de produire des logiciels plus lisibles, plus maintenables dans le temps et intrinsèquement plus scalables, qui sont capables de s'adapter aux besoins changeants des entreprises.

Pourtant, le domaine de l'Infrastructure as Code (IaC) se trouve à un stade différent de sa maturité. Malgré son apparition il y a environ une dizaine d'années, la pratique consistant à gérer et provisionner l'infrastructure informatique via du code est encore en peu mature et en pleine évolution. Elle a transformé la manière dont nous interagissons avec nos infrastructures, permettant une automatisation plus fine et une reproductibilité sans précédent des environnements. Cependant, le rythme effréné de son évolution a également engendré une prolifération d'outils (Terraform, Pulumi, AWS CDK, Ansible, pour n'en citer que quelques-uns) et de méthodes basées sur les préférences personnelles.

Le résultat de cette croissance rapide et souvent non coordonnée, est que les projets d'Infrastructure as Code se développent fréquemment sans un cadre architectural clair. Cette absence de structure et de lignes directrices partagées peut rapidement engendrer une accumulation significative de dette technique, une complexité croissante lors des déploiements, et un manque flagrant d'uniformité entre les différents projets d'une même organisation. À cette problématique s'ajoutent les défis rencontrés par le développement logiciel avant l'adoption généralisée de patterns architecturaux et de bonnes pratiques.

C'est dans ce contexte que se pose une question fondamentale : comment pouvons-nous identifier, formaliser et structurer des bases solides et réutilisables pour nos projets d'Infrastructure as Code, et celà en s'inspirant des succès obtenus dans le monde du développement logiciel ? Il s'agit de définir quels patterns d'architecture et quelles bonnes pratiques pourraient être systématisés pour améliorer drastiquement la lisibilité, la maintenabilité et la durabilité de nos projets IaC.

Pourquoi l'adoption de Patterns est-elle un impératif en Infrastructure as Code ?

L'adoption de patterns architecturaux et de bonnes pratiques en IaC n'est pas qu’une simple "bonne idée" ; elle devient une nécessité opérationnelle et stratégique pour toutes les organisations gérant des infrastructures cloud à grande échelle. Les raisons principales tiennent à la nature même de la complexité et de la dette technique en IaC.

La Complexité Inhérente à l'IaC

La complexité, en informatique, se manifeste lors de l’incapacité à la compréhension du fonctionnement d’un système de façon claire et rapide. En IaC, cette complexité peut être amplifiée notamment par des spécificités de l’iac. Voici deux exemples de complexité en IaC :

  1. La Gestion des États (State Management):

La plupart des outils IaC, notamment Terraform, maintiennent un "état" de l'infrastructure déployée appelé State. Cet état est un enregistrement de l'infrastructure réelle qui est actuellement provisionnée, il est utilisé pour comparer l'état souhaité (décrit dans le code) avec l'état actuel pour avoir une analyse des changements à venir lors de l’application du code.

Les désynchronisations ou les erreurs dans la gestion de cet état peuvent entraîner des comportements imprévisibles et extrêmement difficiles à déboguer. Par exemple, si l'état local ne reflète plus l'état réel de l'infrastructure cloud (suite à une modification manuelle non suivie par IaC, ou un problème de verrouillage d'état), l'exécution de l'IaC peut provoquer des suppressions involontaires de ressources ou des créations en double.

La gestion des verrous d'état (state locking) et le stockage sécurisé de cet état (par exemple, dans un bucket S3 versionné avec un verrouillage DynamoDB) sont essentiels mais ajoutent à la complexité de la mise en œuvre.

  1. La Collaboration en Équipe:

Lorsque plusieurs développeurs ou équipes travaillent simultanément sur la même base de code IaC, la coordination devient un défi majeur. Sans des pratiques de collaboration claires, des conventions de code strictes et des outils de contrôle robustes, les risques de conflits lors des fusions de code (merge conflicts) ou de modifications contradictoires sont élevés.

Les modifications non coordonnées peuvent entraîner des "dérives de configuration" (configuration drift), où l'état réel du state de l'infrastructure s'éloigne de ce qui est défini dans le code, rendant les déploiements futurs imprévisibles.

Pour maîtriser cette complexité, il est impératif d'adopter des bonnes pratiques telles que la modularisation du code, l'automatisation des tests, une documentation rigoureuse et l'utilisation de conventions de nommage claires.

La Dette Technique en Infrastructure as Code

La dette technique se manifeste en IaC lorsque des choix de conception rapides ou des compromis sont faits, sacrifiant la qualité ou la structure du code pour une livraison accélérée des fonctionnalités. Ces raccourcis peuvent sembler efficaces à court terme, mais ils entraînent des coûts supplémentaires et une complexité accrue à long terme. En IaC, la dette technique peut être due à plusieurs facteurs notamment par exemple :

  1. Ignorance des Bonnes Pratiques : Ne pas suivre les conventions de nommage (qui sont cruciales pour l'audit et la gestion des coûts ), les principes de sécurité par défaut ("security by design"), ou les recommandations spécifiques des fournisseurs cloud peut rendre l'infrastructure moins robuste, moins sécurisée et potentiellement plus coûteuse.
  2. Absence de Contrôle de Version Rigoureux: Ne pas utiliser correctement un système de contrôle de version comme Git pour l'IaC, ou pire, autoriser des modifications manuelles directes sur l'infrastructure sans qu'elles soient reflétées dans le code IaC. Cette "dérive de configuration" est une forme majeure de dette technique, car le code ne représente plus la source de vérité.
  3. Manque de Modularisation et de Réutilisation : La création de "monolithes IaC" où toute l'infrastructure est définie dans un seul fichier ou un ensemble de fichiers très étroitement liés rend le code difficile à comprendre, à maintenir et à faire évoluer. Le manque de modules réutilisables conduit à la duplication de code et à des incohérences.

Les conséquences de cette dette technique et de la complexité associée sont lourdes:

  • Coûts Opérationnels Directs: Le temps passé à déboguer des problèmes, à comprendre un code illisible, ou à gérer des infrastructures incohérentes se traduit par des coûts directs en termes de temps humain (1 jour par semaine du temps d’un développeur selon certains articles) mais aussi en coût monétaire avec 28% du budget informatique qui est alloué à la gestion de la dette technique.
  • Impact sur l'Agilité et la Stratégie : Une IaC complexe et pleine de dette technique ralentit le temps à déployer en production. La difficulté à modifier ou à étendre le code ralentit les cycles de déploiement, impactant la capacité de l'entreprise à innover. L'infrastructure devient rigide, difficile à faire évoluer face à la croissance ou aux changements stratégiques.
  • Risques Accrus : La complexité peut masquer des vulnérabilités de configuration, rendant difficile l'application cohérente des politiques de sécurité. Cela augmente le risque d'incidents de sécurité et impacte la fiabilité de l'infrastructure.

Il est clair que la gestion proactive de cette dette technique est essentielle pour garantir la pérennité et l'efficacité des infrastructures modernes.

Notre Pattern : La "Clean Infrastructure"

Pour gérer une grande partie de ces problèmes, nous avons imaginé un pattern pour la structuration et l’organisation du code, son objectif est simple. Essayer de faire la meilleure infrastructure possible dans le temps.

Mais la meilleure infrastructure c’est quoi ?

Tout dépend de vos attentes mais pour le code on peut faire apparaître des caractéristiques communes qu’on retrouve aussi dans d’autres pattern d’informatique comme la clean architecture, ainsi en se basant sur celle-ci on obtient une architecture propre à une infrastructure :

Architecture du pattern de clean infrastructure

Schéma d’organisation de la clean infrastructure.

Un peu d’explication, commençons par le haut, les inputs représente le main de l’infrastructure, ils reçoivent notamment le choix de l’environnement, les différentes configurations, et les inputs/new de l’ensemble des ressources et des use cases afin de pouvoir respecter le principes d’inversion de dépendances, c’est ici que les choix manuelles sont fait et pris en compte.

À partir de là, tout est dynamique et est dépendant des choix effectués avant, notamment pour la configuration.

On retrouve ensuite le contrôleur qui gère l’ordre de déploiement des ressources, il appelle les use case au bon moment et inclut les variables dynamiques tels que les endpoints qui sont passés en entré comme variables dans la configuration qui est utilisé par le use case pour la création de la ressource.

Les usecase sont alors appelés un à un, vont transmettre à leur interface la bonne configuration pour le bon type de ressource.

Cela se termine par la création de la ressource, qui en fonction des choix initiaux, est déployée sur le provider choisi avec la configuration demandée pour chaque ressource.

Le trajet d’un use case jusqu'à la création de ressources ont été conçus comme un module. Ainsi, en passant n’importe quelle configuration à notre use case, nous pourrions créer la bonne ressource en fonction de nos choix.

Et donc, pour créer une ressource de type A, je dois donc appeler le use case correspondant dans le controller avec la configuration souhaitée et une nouvelle ressource est créée. Le résultat obtenu empêche la duplication de code et celui-ci n'est enrichi qu'avec des configurations.

Comme vous l’aurez peut être remarqué, la partie haute du pattern ne dépend d’aucun provider. Ce choix a été fait pour non seulement faciliter la migration vers un autre cloud provider, mais il permet aussi de déployer l’infrastructure en local pour faire ses tests rapidement. On peut même implémenter plusieurs clouds provider si la folie nous prend, et cela,de façon rapide sans impacter les autres providers.

Ainsi, on a une séparation des ressources pour faciliter la compréhension, la modification et la gestion de celle-ci. On limite les effets de bords et chaque équipe peut gérer sa ressource sans avoir un impact sur une autre ressource.

On retrouve alors une architecture théorique qu’il faut maintenant tester pour valider nos théories.

De la théorie à la pratique

Vous trouverez ci-dessous le lien du gitlab avec l’ensemble du code :

LIEN GITLAB

J’ai eu la chance de pouvoir tester ce pattern en créant une infrastructure as code pour une application existante, bien qu’une petite application, elle m’a montré les forces et faiblesses de notre pattern et les différents cas d’usage.

Parlons un peu de l’application, elle se nomme Ochocast, elle a pour objectif de pouvoir stocker et gérer des vidéos ou encore créer des événements notamment pour l’Octofest.

L’infrastructure que nous allons devoir déployer doit être, comme le pattern l’indique, complètement isolée ainsi il fallait adapter certains composants de l’architecture initial pour les faire fonctionner dans des containers. Ainsi on se retrouve avec l’infrastructure suivante :

On observe 3 containers, le backend avait initialement un dockerfile, le front react qui est déployé avec nginx doit recréer son image à chaque déploiement en prenant en compte les variables dynamiques des ressources pulumi. Et enfin, KeyCloak qui permet une gestion des utilisateurs. À cela s'ajoutent les différents objets de stockage pour nos ressources.

Le main se trouve dans “PulumiResourceDeployment”, c’est ici que sont effectués les différents inputs avec notamment le choix du provider et la prise en compte de la configuration des ressources qui se trouve dans le dossier config.

Les différents use case sont accompagnés, juste au-dessus, de types qui sont composés en 2 parties: les inputs et outputs des use cases, interfaces et ressources de chaque provider. Leur classe mère est donc composée de variables communes entre tous les providers. Par exemple, le nom et port pour le type containers et leur sous classes ajoutent des variables en fonction du cloud provider, comme le cluster ecs pour aws. On garde ainsi une indépendance au provider jusqu’au dernier moment. Lors de la création de ressources, le type passe de la classe mère à la classe fille spécifique.

Nous avons ensuite les Uses cases et interfaces qui servent d'intermédiaire entre les différentes couches.

On observe alors la découpe entre les différentes ressources du cloud provider, le controller et notre input.

On peut aussi effectuer des tests sur l’infrastructure, 2 types de tests existent :

- Les tests unitaires qui MOCK les ressources afin d'éviter le déploiement des ressources et accélérer la phase de test et cela grâce à Jest.

- Les tests d’intégration qui eux utilisent runInPulumiStack, qui appel le code typescript pour le testé avec des appels systèmes.

Bien que structuré de façon claire et logique, on peut quand même observer une complexité initiale plus importante. Ainsi, même si cela permet une séparation claire entre les différentes ressources, la compréhension globale du projet peut s'avérer plus compliquée et complexe.

On retrouve aussi la gestion de la dynamicité qui est une difficulté. Vouloir créer les ressources dynamiques sans connaître leur point d’accès final par exemple, demande une adaptation du code à gérer et créer ses ressources de la bonne façon. Il ne faut pas qu’une ressource soit configurée avant qu'elle ne soit créée. Et la configuration de certaines ressources nécessite de connaître ces endpoints avant leur création, on se retrouve alors avec la nécessité de passer par des intermédiaires comme des load balancers ou alors de les connaître en les fixant avec des noms de domaines fixes par exemple.

On peut donc dire que ce pattern ne s’adapte pas à toutes les infrastructures et ne répond pas à tous les problèmes. Bien que réduisant la complexité pour les gros projets, cette approche peut être inutile pour des petits projets voué à le rester.

Conclusion : Vers une Culture d'Architecture IaC Partagée

L’Infrastructure as Code est aujourd’hui un pilier incontournable de la gestion moderne des systèmes d’information, mais elle souffre encore d’un manque de maturité sur le plan architectural. Face à une complexité croissante, à une dette technique difficilement maîtrisable et à des exigences de scalabilité et de sécurité de plus en plus élevées, il devient impératif de structurer nos approches.

En s’inspirant des patterns éprouvés du développement logiciel, comme la Clean Architecture, le pattern Clean Infrastructure propose une structuration claire, modulaire et évolutive de l’IaC. Il répond à des besoins concrets : réduction de la dette technique, amélioration de la lisibilité, facilitation de la collaboration, et accélération des cycles de livraison. Il ne faut pas oublier que ce pattern ne s'adresse pas à tous comme montré précédemment. Pour comprendre sa nécessité, il faut avoir un projet à grande complexité. L'application de cette méthode perd son sens pour des petits projets.

Mais au-delà du pattern lui-même, c’est une culture d’architecture partagée qu’il faut instaurer, une culture où l’IaC est conçue, revue et maintenue avec la même rigueur que le code applicatif. En effet, même rigueur que le code applicatif nécessite l'adoption, l'adoption de bonnes pratiques, la formalisation de standards d’équipe, et un effort collectif pour rendre l’infrastructure aussi maintenable que le logiciel qu’elle soutient.

L’IaC n’est pas qu’un outil technique : c’est un levier stratégique. Et comme tout levier stratégique, il mérite une fondation solide, un langage commun, et une vision partagée.