Pourquoi structurer Terraform en modules
Lorsqu'une infrastructure cloud dépasse le stade prototypaire, le code Terraform devient rapidement un ensemble de déclarations hétéroclites qui se répètent d'un environnement à l'autre. Chaque développeur applique ses propres conventions, les variables sont mal documentées, et ajouter une nouvelle ressource ou dupliquer une architecture coûte des jours de travail manuel et d'ajustements. Les modules Terraform résolvent ce problème en encapsulant des groupes cohérents de ressources dans des unités réutilisables, testables et versionnées. Au lieu de copier-coller des blocs de configuration ou d'écrire la même logique d'interconnexion entre une VPC, des subnets et des groupes de sécurité plusieurs fois, vous écrivez une seule fois, puis vous l'instanciez autant que nécessaire en passant des paramètres différents. Cette approche réduit drastiquement la surface d'erreur, accélère le déploiement de nouveaux environnements et facilite la maintenance centralisée. Pour une ETI ou une scale-up qui opère sur AWS, passer à une architecture modulaire Terraform est souvent le moment où l'infrastructure cesse d'être une accumulation artisanale de clickops et devient un vrai codebase d'équipe, avec traçabilité, révision et continuité.
Architecture des modules Terraform et conventions de nommage
Un module Terraform est simplement un dossier contenant au minimum trois fichiers : main.tf (déclaration des ressources), variables.tf (les paramètres d'entrée) et outputs.tf (ce que le module expose à l'extérieur). La convention recommandée par HashiCorp et largement adoptée dans l'industrie place les modules dans un dossier modules/ à la racine du projet, chaque sous-dossier correspondant à un domaine logique : modules/networking pour tout ce qui touche VPC, subnets et routage, modules/security pour IAM et security groups, modules/compute pour EC2, autoscaling, et ainsi de suite. Chaque module doit avoir un nom court, significatif et en minuscules avec underscores (ex : alb_application, rds_postgresql, iam_role_lambda). À l'intérieur d'un module, les variables d'entrée doivent être explicitement typées et documentées via des descriptions claires, jamais laissées en inférence de type ou sans contexte. Les outputs doivent retourner exactement ce qu'un autre module ou la configuration root aurait besoin de connaître (IDs, ARNs, endpoints) sans exposer d'informations redondantes. Pour les groupes de modules qui travaillent ensemble, une convention de nommage cohérente des ressources Terraform au sein d'un projet aide énormément : si vous préfixez vos ressources AWS par le nom du module et le contexte d'environnement (ex : networking_prod_vpc, security_prod_alb_sg), vous évitez les collisions de noms et rendez les configurations faciles à parcourir et déboguer en production. Les chemins de version doivent également être documentés : un module stable s'utilise avec une version spécifique (ex : source = "./modules/networking?ref=v1.2.0" en local, ou source = "git::https://git.monentreprise.com//terraform-modules.git//networking?ref=v1.2.0" si vous hébergez vos modules dans un dépôt Git privé).
Variables, outputs et interaction entre modules
Les variables.tf d'un module doivent être rédigées de manière à que n'importe quel autre ingénieur puisse utiliser le module sans lire le code de main.tf. Chaque variable doit avoir un type déclaré (string, number, list, map, object ou une combinaison), une valeur par défaut si elle n'est pas obligatoire, et une description suffisamment détaillée pour indiquer le format attendu, les exemples acceptés et les contraintes éventuelles. Par exemple, au lieu de simplement écrire "variable instance_type", écrivez une variable avec description "Type d'instance EC2 (ex : t3.micro, t3.small) ; vérifier les zones de disponibilité supportées dans l'environ actuel" et type = string. Les outputs.tf doit contenir uniquement ce qui sera consommé par un autre module ou par la configuration root. Si vous créez une instance EC2, exposez son ID et son adresse IP privée, mais pas son configuration interne en détail. Une bonne pratique consiste à versionner les outputs : si une sortie n'est plus utilisée ou change de sens, créez une nouvelle sortie plutôt que de réécrire l'ancienne, pour ne pas casser les dépendances en aval. Quand un module appelle un autre module (composition de modules), les outputs du premier deviennent les variables d'entrée du second. Terraform gère automatiquement cet ordre d'exécution via son graphe de dépendances, mais vous devez documenter explicitement ces dépendances. Si votre module compute a besoin d'un security group, assurez-vous que le module qui le fournit (ex : modules/security) est appellé en premier et que vous repassez ses outputs au module compute en tant que variable d'entrée. Une bonne pratique pour éviter les couplages trop serrés est d'utiliser des variables de type map ou object pour grouper les paramètres liés, plutôt que d'exposer vingt variables individuelles : un module peut alors accepter une structure unique qui se décompose en paramètres internes, rendant la configuration root beaucoup plus lisible.
Gouvernance, versioning et réutilisabilité à l'échelle
Une fois vos modules stabilisés, la vraie gouvernance commence : comment s'assurer que tous les projets et environnements les utilisent correctement, que les versions sont contrôlées et que les mises à jour ne cassent rien en production. La première étape est de héberger vos modules dans un système de contrôle de version (Git, avec dépôt privé sur GitLab, GitHub ou Gitea selon votre stack). Chaque module doit avoir un changelog clair, une versioning sémantique (v1.0.0, v1.1.0, v2.0.0) et des tags Git. Un module en version v1.2.3 doit rester stable ; si vous faites une breaking change, c'est v2.0.0. Cela signifie que n'importe quel autre projet peut pointer sur une version fixe et n'être affecté que s'il choisit explicitement d'upgrader. Dans Terraform, utilisez soit le chemin local (source = "./modules/xxx") pour un monorepo où tous les modules et leurs consommateurs sont dans le même dépôt, soit un chemin Git (source = "git::https://...terraform-modules.git//networking?ref=v1.2.0") pour un dépôt dédié aux modules. Les monorepos sont plus faciles à maintenir au début, mais deviennent peu pratiques si plusieurs équipes ou projets doivent évoluer à des rythmes différents ; un dépôt de modules centralisé offre plus de flexibilité. Pour la gouvernance dans un dépôt Git de modules, utilisez les pull requests : chaque modification d'un module doit passer par une review d'un pair, avec une vérification que les tests Terraform passent (terraform validate, terraform plan) et que la documentation est à jour. Utilisez terraform fmt -recursive pour imposer une mise en forme cohérente et un linter (ex : tflint) pour détecter les erreurs ou les patterns non conformes à votre politique (ex : absence de tags AWS, VPC non chiffrées par défaut). Enfin, documentez chaque module avec un README.md clair : qu'est-ce que le module fait, quels sont les prérequis AWS (ex : VPC déjà créée), comment l'utiliser avec des exemples concrets, et quelles sont les limites ou les dépendances externes. Cette documentation doit être maintenable, avec un script qui regénère automatiquement les sections inputs/outputs à partir des fichiers variables.tf et outputs.tf pour éviter les inconsistances.
Tests et validation des modules Terraform
Un module Terraform sans tests est un module fragile. Comme n'importe quel code, il doit être validé avant d'être mis en production. Les trois niveaux de validation pour Terraform sont : d'abord, terraform validate qui vérifie la syntaxe et la cohérence interne du code (types de variables, références entre ressources, structure) ; ensuite, terraform plan qui calcule la divergence réelle entre l'état local et l'état AWS, déjectant ainsi les erreurs logiques ou de permissions IAM sans appliquer de changement ; enfin, des tests d'intégration qui exécutent réellement terraform apply sur un environnement de test, vérifient que les ressources créées fonctionnent correctement, puis les détruisent. Pour les modules, des frameworks comme Terratest (en Go) permettent d'écrire des tests d'intégration reproductibles : vous décrivez un scénario (ex : créer un cluster RDS via le module, vérifier qu'il est accessible, supprimer), puis Terratest orchestré dans votre CI/CD garantit que ce scénario passe à chaque version. Une bonne pratique est de créer des fichiers examples/ dans le dossier du module, contenant des configuration root complètes et réalistes qui utilisent le module avec différentes combinaisons d'inputs, pour documenter les cas d'usage et servir de base aux tests d'intégration. Pour chaque exemple, vous exécutez terraform apply, puis vous vérifiez les outputs attendus, puis terraform destroy pour nettoyer. Dans une équipe DSI ou DevOps d'une scale-up, cette approche systématique des tests transforme la maintenance des modules d'une tâche pénible (upgrader un module et attendre les bugs de production) en une tâche fluide où vous savez qu'une version a été validée sur tous les cas d'usage documentés. Elle libère également du temps pour se concentrer sur l'évolution de l'architecture plutôt que sur le débogage réactif.
Cas d'usage réels : structurer vos modules pour AWS
En pratique, pour une ETI ou une scale-up sur AWS, voici comment structurer vos modules pour couvrir les cas d'usage les plus courants. Commencez par un module networking qui encapsule la VPC, les subnets publics et privés, les route tables, et la NAT gateway : il expose les IDs de subnets et la VPC ID que vous repasserez aux autres modules. Ensuite, un module security qui gère les security groups (inbound/outbound rules) pour différents workloads (web, database, cache) : plutôt que de créer un security group par instance, vous en créez un par rôle fonctionnel et l'instanciez plusieurs fois avec des règles configurables. Un module compute peut encapsuler EC2 + user data, ou ALB + target groups, ou Lambda + IAM role et environment variables, ou ECS cluster + task definitions, selon votre contexte. Un module database peut gérer une RDS instance multi-AZ avec backup policy, monitoring CloudWatch, et accès sécurisé via security group. Un module storage pour S3 buckets avec versioning, chiffrement et policies d'accès. Chacun de ces modules doit être assez général pour s'adapter à plusieurs cas d'usage (ex : le module database supporte PostgreSQL, MySQL, MariaDB via des variables type_engine), mais assez spécifique pour que quelqu'un qui l'utilise n'ait pas à connaître les détails internes d'AWS. Si votre module network crée automatiquement une NAT gateway avec une Elastic IP, vous simplifiez la vie à 90% de vos utilisateurs ; si vous les forcez à la configurer eux-mêmes, 50% l'oublieront ou la misconfigureront. Enfin, une configuration root au sommet du projet ou du dépôt appelle ces modules dans l'ordre correct, passe les outputs d'un à un autre, et documente comment provisionner un nouvel environnement complet en trois commandes Terraform. Ce pattern transforme l'onboarding d'un nouvel ingénieur : au lieu de comprendre 500 lignes de configuration Terraform implicite, il voit clairement quels modules sont utilisés, avec quels paramètres, et peut en extrapoler la logique d'architecture.
Intégration des modules dans vos pipelines CI/CD
Une fois vos modules stabilisés et testés, ils doivent s'intégrer dans vos pipelines de déploiement existants. Si vous utilisez GitHub Actions, GitLab CI ou Jenkins, chaque étape du déploiement doit faire appel à terraform plan et terraform apply avec les bonnes variables et le bon backend (état Terraform centralisé en S3 avec verrous DynamoDB). Lorsqu'un changement de module arrive dans main ou release, le pipeline doit réexécuter les exemples et les tests d'intégration avec les nouvelles variables et les nouveaux modules. Cela signifie que votre backend Terraform (par défaut un bucket S3) doit être partagé ou au minimum organisé par environnement (dev, staging, prod) et par projet, pour que chaque équipe ne soit pas bloquée par les locks d'une autre. Pour les modules réutilisables, une bonne pratique est d'utiliser Terraform workspaces ou des variables d'environnement passées au module (ex : var.environment = "prod" vs "staging") pour que le même code de module déploie des ressources correctement isolées pour chaque environnement. Enfin, si vous avez plusieurs équipes ou projets, envisagez une couche d'orchestration (ex : Terraform Cloud ou Terraform Enterprise de HashiCorp, ou un wrapper en interne) qui ajoute des contrôles de gouvernance : approvals obligatoires avant apply en prod, audits de qui a appliqué quoi et quand, coûtes estimés pour chaque terraform plan. Cela transforme votre infrastructure de code artisanal à code d'équipe industrialisé, où chaque changement est traçable, reversible et conforme aux politiques d'entreprise.