Blog | WeScale

De l'IaC à l'Infrastructure as Product : sécuriser la Supply Chain Terraform de bout en bout

Rédigé par Baptiste Chauvelier | 06/05/2026

La fin du “Works on my machine”

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.

1. Le problème de la "Boîte Noire"

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.

2. Le syndrome du "Flocon de Neige"

"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é :

  • Lucie utilise Terraform 1.5.0, tandis que Paul a mis à jour sa CLI en version 1.6.0.
  • Jacques a une version spécifique de aws-cli ou des binaires système différents.
  • L'un travaille sous macOS (architecture ARM), l'autre sous Linux.

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.

3. La sécurité : Une surface d'attaque incontrôlée

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.

Vers le "Zero-Touch Deployment"

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.

Étape 1 : Forger une "Tooling Factory" souveraine et sécurisée

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.

1. La maîtrise de la composition

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 :

  • Le Runtime : Node.js.
  • L'Orchestration : Les plugins Semantic Release (@semantic-release/gitlab, exec, git, etc) verrouillés via pnpm-lock.yaml.
  • Les Utilitaires Système : 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.

2. "Trust but Verify" : Les tests structurels

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 :

  1. Build & Load : Docker Buildx construit l'image et la charge dans le démon local du runner (sans la pousser).
  2. Test : Une batterie de tests vérifie la présence des binaires, les permissions des fichiers et l'exécution des commandes de base (semantic-release --version).
  3. Push : L'image n'est poussée vers le registre que si, et seulement si, tous les tests sont verts.

3. La chaîne de garde : Signature cryptographique avec Cosign

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 :

  1. GitLab émet un jeton d'identité unique certifiant que "C'est bien le job X du projet Y qui s'exécute".
  2. Cosign utilise ce jeton pour générer un certificat éphémère et signer le Digest unique (le hash SHA256) de l'image.
  3. La signature est stockée dans le registre à côté de l'image.

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.

4. La performance au service de l'expérience développeur

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.

Étape 2 : L'usine à modules, ou l'infrastructure traitée comme un produit logiciel

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.

1. La qualité comme barrière à l'entrée (Fail Fast)

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.

  • Linting (TFLint) : Nous ne vérifions pas seulement la syntaxe, mais les bonnes pratiques spécifiques aux providers (ex: type d'instance AWS invalide).
  • Sécurité (TFSec / Trivy) : Le code est scanné pour détecter les mauvaises configurations avant déploiement (bucket S3 public, chiffrement manquant).
  • Formatage : Le 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".

2. L'automatisation du versioning avec Semantic Release

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) :

  • Un commit fix: correct sg rule déclenchera automatiquement une version Patch (1.0.0 -> 1.0.1).
  • Un commit feat: add rds support déclenchera une version Mineure (1.0.0 -> 1.1.0).
  • Un commit avec 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.

3. Le Registre Privé : Une source de vérité unique

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é).

Étape 3 : L'Assemblage final, ou la quête du déploiement déterministe

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é.

1. Le verrouillage absolu des dépendances (Pinning)

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 :

  • Modules : Nous appelons nos modules internes via le registre GitLab avec des versions figées (version = "1.2.0"). Jamais de latest ou de branches Git mouvantes.
  • Providers : Le fichier .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.

2. La séparation sacrée : Plan vs Apply

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.

  1. Le job Plan génère un fichier binaire (terraform plan -out=tfplan). Ce fichier est “une photo” de l'action prévue.
  2. Ce fichier est sauvegardé comme artefact sécurisé par GitLab CI.
  3. Le job Apply récupère cet artefact et l'exécute (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é.

3. La "Release" comme conséquence du succès

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.

4. Sécurité : L'exécution sous haute surveillance

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.

Étape 4 : L'hygiène des secrets, externalisation et injection dynamique

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 :

  • L'image Docker est agnostique : Comme vu à l'étape 1, notre image "Tooling" ne contient aucun fichier de configuration ou .env. Elle est générique et publique au sein de l'entreprise.
  • Les coffres-forts comme source de vérité : Nous n'utilisons plus de variables pour les secrets, mais des références. Les secrets sont stockés dans des services dédiés (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault).
  • L'Injection au Runtime :
    • Soit Terraform récupère lui-même le secret via une data source au moment de l'exécution.
    • Soit les secrets sont injectés par la CI/CD sous forme de variables d'environnement masquées (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.

Étape 5 : Fermer la boucle, ou la maintenance en autopilote

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.

1. La "Méta-Maintenance" : Quand la CI met à jour la CI

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.

  1. Détection : Renovate scanne le dépôt de notre "Tooling Factory" et détecte que le package.json peut être mis à jour.
  2. Action : Il ouvre automatiquement une Merge Request.
  3. Réaction : Cette MR déclenche instantanément notre pipeline de build d'image Docker (décrit à l'étape 1). L'image est construite, testée structurellement, signée et poussée.
  4. Résultat : Sans aucune intervention humaine autre qu'un clic de validation ("Merge"), notre pipeline utilise désormais une version patchée et sécurisée de ses propres outils.

2. La gestion indolore du Lockfile Terraform

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 :

  • Il met à jour la version dans le code.
  • Il régénère le .lock.hcl correctement.
  • La CI se lance et effectue un 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.

3. La propagation fluide des modules internes

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.

Conclusion : De l'exécution manuelle à l'Industrialisation de l'IaC

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 :

  1. Vitesse : Les développeurs consomment des modules prêts à l'emploi, versionnés et documentés.
  2. Sérénité : Le déploiement est déterministe. Le "vendredi après-midi" n'est plus un moment angoissant.
  3. Sécurité : La surface d'attaque est réduite au strict minimum grâce à une Supply Chain maîtrisée.

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.