L'adoption de l'Infrastructure as Code (IaC) avec Terraform a été une révolution pour nos opérations. Pourtant, dans de nombreuses équipes, l'exécution de ce code reste étrangement artisanale. Le code est versionné, certes, mais le déploiement se fait encore trop souvent depuis le terminal d'un ingénieur, avec un simple terraform apply lancé en local.
Cette pratique, bien que commode en apparence, masque trois risques majeurs qui fragilisent toute la chaîne de production.
C'est le scénario classique du mardi soir : un développeur lance un déploiement depuis son poste avant de partir. Le lendemain matin, la production est instable. Le problème ? Il n'existe aucune trace centralisée. Qui a lancé la commande ? Avec quels arguments ? Sur quelle version du code exactement ? Dans ce contexte, le "rollback" devient un jeu de devinettes dangereux, car nous ignorons l'état exact de l'infrastructure avant l'incident.
"Mais ça marchait sur mon poste !". Cette phrase, devenue un mème dans le développement logiciel, s'applique tragiquement à l'infrastructure. La réalité du déploiement local, c'est l'hétérogénéité :
1.5.0, tandis que Paul a mis à jour sa CLI en version 1.6.0.aws-cli ou des binaires système différents.Ces micro-différences finissent inévitablement par créer des comportements imprévisibles en production. Si l'environnement d'exécution n'est pas standardisé, le résultat du déploiement est aléatoire.
Enfin, le déploiement local implique une aberration de sécurité : pour qu'il fonctionne, chaque ingénieur doit posséder des clés d'administration (AWS, Azure, GitLab) stockées en clair sur son disque dur (~/.aws/credentials). C'est une violation flagrante du principe de moindre privilège. Un laptop volé, perdu ou compromis donne potentiellement accès aux clés du royaume. De plus, ces accès "humains" contournent souvent les mécanismes de validation (Policy as Code, scans de sécurité) que nous essayons d'imposer.
Pour répondre à ces défis, il faut adopter une philosophie radicale : "Si ce n'est pas dans la CI, ça n'existe pas."
Le pipeline CI/CD ne doit plus être une option, mais le seul point d'entrée pour interagir avec l'infrastructure. Dans cet article, je vous détaille comment construire une véritable usine logicielle pour Terraform — auditables, immuable et sécurisée — en nous appuyant sur une Supply Chain maîtrisée de bout en bout : de l'image Docker signée jusqu'au déploiement en production.
Si nous interdisons aux développeurs d'exécuter des commandes depuis leurs postes, nous devons leur fournir un environnement d'exécution de substitution qui soit irréprochable. En CI/CD, la reproductibilité du pipeline dépend entièrement de la reproductibilité de l'image Docker qui l'exécute.
L'anti-pattern classique consiste à utiliser des images publiques génériques (node:latest, hashicorp/terraform:light) et à les "patcher" à la volée dans le pipeline via des commandes apk add ou npm install. Cette approche est fragile : elle ralentit l'exécution, dépend de la disponibilité des dépôts externes et, surtout, ne garantit pas que le binaire git utilisé aujourd'hui sera le même demain.
Pour notre usine logicielle, nous avons opté pour une approche "Golden Image" immuable.
Notre première exigence est la maîtrise des ingrédients. Nous avons adopté une stratégie ciblée pour nos environnements d'exécution.
Pour la gestion des versions : Nous avons construit une image Docker "Tooling" dédiée exclusivement à semantic-release. L'objectif est d'éviter l'installation coûteuse des dépendances à chaque exécution du job. Basée sur Alpine Linux, elle pré-embarque :
@semantic-release/gitlab, exec, git, etc) verrouillés via pnpm-lock.yaml.git, ssh, curl.Pour les opérations Terraform : Nous nous appuyons sur les images officielles des providers.
Dans les deux cas, nous ne laissons aucune place au hasard. Plutôt que d'utiliser des tags de version mouvants (comme :latest ou :1.0), nous référençons systématiquement les digests SHA256. Cela nous garantit une chaîne de construction basée sur des fondations connues, auditables et immuables.
Construire une image ne suffit pas ; il faut garantir sa viabilité avant qu'elle n'atteigne le registre. Rien n'est plus frustrant pour un développeur qu'un pipeline qui échoue après 2 minutes parce qu'il manque curl dans l'image.
Nous avons implémenté une étape de validation structurelle au cœur du pipeline de build, utilisant l'outil container-structure-test. Le flux est le suivant :
semantic-release --version).C'est ici que nous entrons dans l'ère de la Supply Chain Security. Comment garantir que l'image utilisée en production est bien celle qui a été validée par notre CI, et qu'elle n'a pas été altérée ou remplacée par une image malveillante ?
Nous utilisons Sigstore Cosign pour signer nos images. Nous avons adopté l’approche ”Keyless” (sans gestion de clés), qui repose sur l'identité OIDC (OpenID Connect) fournie par GitLab. Concrètement, lors de la phase de publication :
Cette signature devient notre sceau d'inviolabilité. Plus tard dans la chaîne, nos pipelines de déploiement refuseront purement et simplement d'utiliser une image dont la signature n'est pas valide.
La sécurité ne doit pas se faire au détriment de la vitesse. Reconstruire une image complète avec compilation de modules Node.js peut être lent. Pour pallier cela, nous exploitons les capacités de cache avancé de Docker Buildx.
En utilisant les options --cache-to et --cache-from avec le backend type=registry, nous stockons les couches intermédiaires de build directement dans le Container Registry de GitLab. Résultat : un build incrémental ultra-rapide qui ne recompile que ce qui a changé, réduisant le temps de feedback pour nos équipes.
Avoir un pipeline d'exécution robuste ne sert à rien si le code qu'il exécute est instable. Dans l'écosystème Terraform, la tentation est grande de copier-coller des blocs de ressources ou de pointer vers des branches Git mouvantes (source = "git::...ref=main"). C'est la recette assurée pour la dette technique et les régressions en cascade.
Pour notre plateforme, nous avons adopté une approche radicalement différente : chaque module d'infrastructure est traité comme une librairie logicielle à part entière, avec son propre cycle de vie, ses tests et, surtout, son versioning sémantique strict.
Avant même de parler de versioning, le code doit prouver sa validité. Dès qu'une Merge Request est ouverte sur un dépôt de module, notre pipeline déclenche une batterie de contrôles statiques. Nous appliquons la philosophie du "Fail Fast" : rejeter le code le plus tôt possible dans la boucle de feedback.
terraform fmt est obligatoire, garantissant une base de code homogène, quel que soit l'auteur.Si l'un de ces garde-fous échoue, le pipeline s'arrête. Aucune version ne peut être créée sur du code "sale".
La gestion manuelle des tags Git (v1.0.1, v1.1.0) est une tâche fastidieuse, sujette à l'erreur humaine et souvent négligée. Pour résoudre cela, nous avons délégué l'intégralité de la gestion des versions à Semantic Release, orchestré par notre image "Tooling" sécurisée.
Le fonctionnement est entièrement piloté par les messages de commit (Convention Conventional Commits) :
fix: correct sg rule déclenchera automatiquement une version Patch (1.0.0 -> 1.0.1).feat: add rds support déclenchera une version Mineure (1.0.0 -> 1.1.0).BREAKING CHANGE déclenchera une version Majeure (1.0.0 -> 2.0.0).Le résultat ? Une approche déterministe totale. Les développeurs ne se soucient plus des numéros de version ; ils se concentrent sur la nature de leurs changements. Le pipeline calcule la version suivante, génère le Changelog automatiquement, crée le Tag Git et publie la release.
Où vont ces modules une fois tagués ? Certainement pas dans un dossier partagé ou un dépôt Git générique. Nous exploitons le Terraform Module Registry natif de GitLab.
En publiant nos modules dans ce registre privé, nous offrons une expérience de consommation "Premium" à nos équipes applicatives. Elles peuvent consommer l'infrastructure comme on consomme un paquet npm ou maven :
module "app_cluster" {
source = "gitlab.com/my-org/eks-cluster/aws"
version = "1.2.0" # Version immuable et stable
}
Cela crée un contrat clair : tant que l'équipe applicative ne change pas le numéro de version, son infrastructure ne bougera pas, même si le module évolue en parallèle. Nous avons ainsi découplé le cycle de vie du producteur (l'équipe Platform qui itère sur les modules) de celui du consommateur (l'équipe App qui cherche la stabilité).
Si les modules (vus à l'étape précédente) sont nos briques standardisées, le Root Module est le plan de construction de la maison. C'est ce dépôt qui définit l'état réel de nos environnements (Staging, Production).
C'est ici que le risque est le plus élevé : une erreur sur un module est un problème de code, une erreur sur le Root Module est un incident de production. Pour mitiger ce risque, notre pipeline de déploiement repose sur trois piliers d'immutabilité.
Dans un Root Module, l'ennemi numéro un est la "mise à jour silencieuse". Nous refusons qu'un déploiement change de comportement simplement parce qu'un provider AWS ou qu'un module communautaire a été mis à jour dans la nuit.
Nous appliquons une politique de verrouillage strict :
version = "1.2.0"). Jamais de latest ou de branches Git mouvantes..terraform.lock.hcl est commité dans le dépôt. Il contient les empreintes cryptographiques (hashes) exactes des binaires des providers (AWS, Azure, etc.).Cela garantit que le pipeline exécuté aujourd'hui utilisera exactement les mêmes binaires que celui de la semaine dernière. Le déterminisme est total.
Notre pipeline CI/CD sépare strictement la phase d'intention (terraform plan) de la phase d'exécution (terraform apply). Mais nous allons plus loin qu'une simple séparation de jobs : nous utilisons le transfert d'artefact binaire.
terraform plan -out=tfplan). Ce fichier est “une photo” de l'action prévue.terraform apply tfplan).Pourquoi est-ce critique ? Si nous relancions simplement terraform apply sans l'artefact, Terraform recalculerait le graphe de dépendances. Entre le moment du plan et le moment de l'apply, l'état du cloud pourrait avoir changé, entraînant des actions imprévues. En appliquant l'artefact binaire, nous avons la garantie cryptographique que ce qui est déployé est strictement identique à ce qui a été planifié et validé.
C'est ici que notre approche diffère des workflows logiciels classiques. Dans le développement d'app, on tague une release, puis on déploie. Dans l'infrastructure, nous inversons parfois ce paradigme pour garantir la cohérence de l'état.
Nous utilisons Semantic Release sur le Root Module, mais avec une subtilité : le tag de version (qui marque l'état de l'infrastructure à un instant T) n'est officialisé que si le déploiement (apply) est un succès. Si le déploiement échoue, le pipeline s'arrête, aucune version n'est créée, et l'équipe est notifiée. Cela nous assure que si le tag v2.5.0 existe dans Git, il correspond à un état d'infrastructure qui a été réellement et correctement déployé. Git reste ainsi le reflet fidèle de la réalité du terrain.
Enfin, rappelons le principe de notre introduction "Zero-Touch". Ce pipeline d'assemblage est la seule entité à posséder les droits d'écriture sur notre compte Cloud de production. L'authentification se fait via OIDC (OpenID Connect) : le Cloud Provider (AWS/Azure/GCP) fait confiance temporairement au Job GitLab CI spécifique, sans qu'aucune clé d'accès permanente (AWS_ACCESS_KEY_ID) ne soit stockée dans les variables GitLab. Même en cas de fuite de la configuration CI, il n'y a aucun secret statique à voler.
L'automatisation du déploiement soulève une question critique : comment fournir des mots de passe de base de données ou des clés API à Terraform sans jamais les exposer ?
Le piège classique de l'IaC est de penser que les variables Terraform (variable db_password) sont sécurisées. C'est faux. Par conception, Terraform stocke les valeurs de toutes les ressources et variables en clair dans le fichier terraform.tfstate. Si un secret est écrit dans le code ou passé via un fichier .tfvars commité, il est compromis.
Notre stratégie repose sur l'externalisation totale :
.env. Elle est générique et publique au sein de l'entreprise.data source au moment de l'exécution.TF_VAR_db_password), qui ne résident qu'en mémoire vive le temps du job.Cette approche garantit que les secrets n'apparaissent jamais dans le dépôt Git, ne sont jamais persistés dans l'image Docker, et limitent leur exposition dans le tfstate au strict nécessaire, réduisant drastiquement les risques de fuite latérale.
Construire une usine logicielle sophistiquée est une victoire, mais elle n'est pas définitive. En informatique, le pire ennemi n'est pas le bug, mais l'entropie. Les versions de Terraform évoluent, des failles de sécurité (CVE) sont découvertes dans les librairies système, et les providers Cloud changent leurs APIs.
Traditionnellement, le maintien en conditions opérationnelles (MCO) de la chaîne CI/CD est une tâche ingrate, souvent repoussée jusqu'à ce que quelque chose casse. Nous avons décidé d'inverser cette dynamique en automatisant la maintenance elle-même grâce à Renovate.
Renovate n'est pas un simple outil de mise à jour de dépendances ; c'est le chef d'orchestre qui ferme la boucle de notre Supply Chain. Il surveille en permanence nos dépôts (Tooling, Modules, Root) et injecte des mises à jour de manière proactive.
Le cas d'usage le plus impressionnant est la mise à jour de notre propre outillage. Imaginez qu'une nouvelle version de semantic-release sorte, corrigeant une faille de sécurité critique.
package.json peut être mis à jour.La sécurité de la Supply Chain Terraform repose sur le fichier .terraform.lock.hcl, qui fige les empreintes des providers (AWS, Azure, Random). Maintenir ce fichier manuellement est pénible : il faut lancer des commandes locales pour calculer les hashs des différentes architectures (AMD64, ARM64).
Renovate automatise intégralement cette corvée. Lorsqu'une nouvelle version du provider AWS est disponible :
.lock.hcl correctement.terraform plan. Si le plan est vert (pas de changement destructif causé par la mise à jour du provider), la mise à jour est prête à être fusionnée. Nous restons ainsi toujours à jour avec les dernières fonctionnalités du Cloud, sans la dette technique accumulée des "updates trimestriels" massifs et douloureux.Enfin, Renovate assure la cohésion de notre écosystème interne. Lorsqu'une équipe Platform publie la version v1.2.0 d'un module (ex: module-eks), les dépôts "Root" qui consomment ce module en version v1.1.0 sont immédiatement notifiés par une MR de mise à jour.
Cela transforme notre infrastructure en un organisme vivant. Les améliorations et correctifs de sécurité développés au niveau des modules se propagent organiquement vers la production, validés à chaque étape par les barrières de qualité de la CI.
Au terme de cette transformation, nous avons fait bien plus qu'écrire des scripts CI/CD. Nous avons changé de paradigme.
Nous sommes passés d'une gestion artisanale, où la réussite d'un déploiement dépendait de la "mémoire musculaire" des ingénieurs et de la configuration de leurs PC, à une démarche industrielle.
Les gains sont tangibles :
C'est cela, la promesse de l'Infrastructure as Product : investir du temps dans la construction d'une usine robuste pour que, par la suite, l'acte de construire devienne une formalité. L'automatisation n'est pas une fin en soi, c'est le levier qui nous permet de garantir la pérennité et la sécurité de notre plateforme Cloud.