Sägetstrasse 18, 3123 Belp, Switzerland +41 79 173 36 84 info@ict.technology

      Terraform @ Scale - Partie 3a: Gestion du Blast Radius

      🔥 Un seul terraform destroy - et soudainement, 15 systèmes clients sont hors ligne 🔥

      Le "destructeur du vendredi après-midi" a encore frappé. 

      Dans cet article en deux parties, nous analysons l’un des plus grands problèmes structurels d’infrastructure, mais aussi l’un des risques les plus sous-estimés de l’Infrastructure-as-Code du point de vue de la gestion.  Car nous aidons les entreprises à minimiser systématiquement les risques liés au Blast Radius. 

      Car la meilleure explosion est celle qui ne se produit pas.

      L’anatomie d’une catastrophe Terraform

      Scénario : Le destructeur du vendredi après-midi

      Il est 16h30, un vendredi. Un développeur souhaite rapidement nettoyer un environnement de test et exécute terraform destroy. Ce qu’il ignore : la VPC à supprimer est référencée via le Remote State par trois autres états qui gèrent des charges de travail en production pour différents clients.

      ⚠️ Le résultat :

      • 15 systèmes clients tombent simultanément
      • Les connexions aux bases de données sont interrompues
      • Les Load Balancers perdent leurs cibles
      • Les systèmes de supervision signalent une panne totale
      • Le week-end est fichu

      Ce scénario n’est pas fictif - il se produit dans la réalité plus souvent qu’on ne le pense. La cause réside généralement dans une combinaison de facteurs :

      • States monolithiques : Trop de ressources dans un seul état engendrent des dépendances ingérables
      • Responsabilités floues : Personne ne sait exactement qui est responsable de quelles ressources
      • Absence de garde-fous : Aucun mécanisme technique pour empêcher des suppressions accidentelles

      L’Infrastructure-as-Code promet contrôle, reproductibilité et efficacité. Mais si les Terraform States sont mal structurés, un terraform destroy ou un terraform apply en apparence anodin peut tourner à la catastrophe. Le Blast Radius – le périmètre des effets involontaires – devient alors rapidement plus vaste que prévu.

      Après notre dernier article sur le principe de Goldilocks pour des tailles de state optimales, nous abordons maintenant l’un des aspects les plus critiques lors du passage à l’échelle avec Terraform : la gestion du Blast Radius. Car la question n’est pas de savoir si quelque chose va mal tourner, mais combien de choses seront affectées lorsque cela arrivera.

      Qu’est-ce que le Blast Radius dans Terraform ?

      Le terme Blast Radius provient à l’origine de la technologie des explosifs et désigne le périmètre dans lequel une explosion cause des dégâts. Dans l’univers de Terraform, il représente la zone de l’infrastructure qui peut être affectée par des modifications, des erreurs ou des pannes.

      La connexion entre différentes ressources repose sur leurs dépendances mutuelles. Terraform génère en interne un Dependency Graph dynamique, qui n’est pas toujours visible de manière complète et intuitive pour l’ingénieur.

      Certaines dépendances sont évidentes. Par exemple, un serveur virtuel a besoin d’un réseau dans lequel il est déployé. Ce réseau est généralement relié à la ressource Compute du serveur par l’ingénieur, ce qui rend cette dépendance non seulement logique, mais aussi documentée dans le code du module Terraform.

      Mais qu’en est-il des dépendances indirectes et générées dynamiquement, qui ne deviennent apparentes qu’à travers les calculs internes de Terraform ? Par exemple, l’adresse IP d’un service web n’est pas directement attribuée au serveur virtuel, mais à une ressource d’adresse IP, laquelle est ensuite connectée à une ressource VNIC, qui elle-même peut - ou non - être associée à un ou plusieurs serveurs virtuels, ou à un Loadbalancer, voire à un pare-feu. Il devient alors difficile de comprendre spontanément comment une modification du masque réseau d’un sous-réseau impactera une adresse IP attribuée depuis son pool.

      Les infrastructures IT deviennent rapidement plus complexes que ce qui était prévu durant la phase de conception, notamment à cause d’une croissance historique et de nombreux changements. Dès lors, on dépend fortement du fait que chaque ingénieur, ainsi que les automatisations dans la pipeline, lisent réellement le plan d’exécution généré par terraform plan qui défile dans la console shell, l’analysent en profondeur et le vérifient pour détecter d’éventuelles erreurs - ce que pratiquement aucun opérateur humain ne fait, par manque de perspective globale, de temps ou de motivation, et encore moins une pipeline CI/CD moyenne.

      Visualisation du Blast Radius :

      • Small States = peu de dépendances directes et indirectes = Petit Blast Radius : une erreur affecte seulement quelques ressources

      • Large States = plus de dépendances directes et indirectes = Grand Blast Radius : une erreur peut avoir des conséquences étendues et imprévisibles.

       

      En pratique, un Blast Radius incontrôlé se manifeste souvent de plusieurs manières :

      • Suppressions involontaires : Un terraform destroy supprime non seulement les ressources ciblées, mais aussi des composants dépendants dans d’autres systèmes. Cela signifie qu’une modification dans un coin du paysage d’infrastructure provoque une explosion dans un autre endroit totalement inattendu. Ce problème survient principalement dans des States volumineux et monolithiques.
      • Pannes en cascade : Une modification d’une ressource centrale entraîne la défaillance de services ou ressources apparemment indépendants. Cela peut être dû à des dépendances, mais aussi à une organisation erronée des ressources dans de mauvais types de données, donc à des erreurs dans le code. J’ai par exemple eu un client qui avait stocké plusieurs centaines d’enregistrements DNS comme ressources individuelles via count au lieu de for_each dans une liste. Lors d’un terraform apply, une des premières entrées de la liste a été supprimée… et plusieurs centaines d’entrées DNS suivantes aussi, car leur index avait changé et Terraform a donc supprimé puis recréé ces ressources. Le problème est devenu critique lorsque le fournisseur de cloud public, en raison de limites strictes de son API, a ensuite recréé les nouveaux enregistrements DNS lentement, par blocs de cinq. Ainsi, presque tous les services du client non concernés se sont retrouvés indisponibles pendant plus d’une heure. Comment expliquer un tel incident au Change Management et au responsable presse ?
      • Dépendances entre States : Les références au Remote State entraînent des effets secondaires inattendus lors des opérations apply ou destroy. Cela signifie qu’une instance Terraform travaille avec des données provenant d’un autre fichier de state que le sien. "Oups, il y avait encore un serveur Bare Metal en production d’un autre client dans ce réseau…"
      • Chevauchements entre locataires : Une modification pour un client impacte par erreur d’autres clients. Pensez par exemple à des règles de pare-feu gérées dynamiquement dans un environnement partagé.
      • Le chaos absolu : Les choses deviennent vraiment intéressantes lorsque plusieurs de ces scénarios se produisent en même temps. Avec un peu de chance, le site est alors entièrement redondant ailleurs.

      Scénarios typiques de Blast Radius

      1. Le désastre du Remote State


      # State A : Fondations réseau
      resource "aws_vpc" "main" {
      cidr_block = "10.0.0.0/16"
      }
      output "vpc_id" {
      value = aws_vpc.main.id
      }
      # State B : Application (référence le State A)
      data "terraform_remote_state" "network" {
      backend = "s3"
      config = {
      bucket = "terraform-states"
      key = "network/terraform.tfstate"
      }
      }
      resource "aws_instance" "app" {
      [...]
      subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0] [...]
      }

      ⚠️ Le problème : si la VPC dans le State A est détruite, le State B perd ses références. Cela conduit à des états incohérents et nécessite un terraform import ou terraform state rm manuel pour réparation. Dans de nombreuses entreprises, cela entraîne en plus des problèmes organisationnels, car les réseaux et les instances serveurs relèvent souvent de silos différents et une communication rapide et constructive lors d’un tel incident n’est pas toujours garantie.

      2. Le chaos multi-mandant

      Un scénario particulièrement critique survient lorsque plusieurs mandants (clients) partagent des ressources dans le même State :

      Exemple : Shared Infrastructure State


      ├── shared-infrastructure/
      │   ├── customer-a-resources.tf
      │   ├── customer-b-resources.tf 
      │   ├── customer-c-resources.tf
      │   └── shared-services.tf

      Une modification pour Customer A peut impacter involontairement Customer B et C. Pire encore : un terraform destroy pourrait affecter tous les clients en même temps.

      C’est pourquoi il est essentiel d’isoler complètement tous les mandants. Certes, cela représente peut-être un coût initial plus élevé. Mais une panne coûte encore plus cher.

      3. L’enfer des dépendances

      Des chaînes de dépendance complexes entre ressources peuvent entraîner des effets en cascade imprévisibles :

      Chaîne de dépendance : 

      Une modification au début de la chaîne peut avoir des conséquences jusqu’à la fin. Cela provoque des effets secondaires inattendus, principalement à cause de dépendances invisibles dans le code, calculées par Terraform.

      La responsabilité incombe ici aux différents Terraform Providers. Les dépendances impliquent en effet que le Provider ne se contente pas de transmettre des valeurs d’entrée à l’API du fournisseur cloud sans les vérifier ni les corréler, mais implémente également un minimum de logique et de bon sens. Et croyez-moi : beaucoup de Providers sont très, très mauvais dans ce domaine. 

       

      Minimiser le Blast Radius : stratégies et bonnes pratiques

      Je vous ai probablement donné quelques sueurs froides. Parlons donc maintenant des solutions pour éviter ce type de problème.

      Approche 1 : Segmentation des States selon le Blast Radius

      La méthode la plus efficace pour contrôler le Blast Radius est une segmentation réfléchie des States. Lors de l’architecture d’une solution d’automatisation, nous nous basons sur les principes suivants :

      Isolation selon le niveau d’impact :

      • Critical States : Infrastructure cœur avec Blast Radius élevé (réseau, IAM, DNS)
      • Service States : Ressources spécifiques aux applications avec Blast Radius moyen
      • Ephemeral States : Ressources temporaires avec Blast Radius minimal

      # Exemple de structure optimisée pour le Blast Radius dans AWS
      ├── foundation/               # Infrastructure critique
      │   ├── network-core/         # VPCs, Transit Gateways (Blast Radius élevé)
      │   ├── security-baseline/    # IAM, KMS Keys (Blast Radius élevé)
      │   └── dns-zones/            # Zones Route53 (Blast Radius moyen)
      │
      ├── platform/                 # Services de plateforme
      │   ├── kubernetes-cluster/   # Clusters EKS/OKE (Blast Radius moyen)
      │   ├── databases/            # RDS, DocumentDB (Blast Radius moyen)
      │   └── monitoring/           # CloudWatch, Grafana (Blast Radius faible)
      │
      └── applications/             # Couche applicative
          ├── frontend-dev/         # Environnement de développement (Blast Radius faible)
          ├── frontend-prod/        # Environnement de production (Blast Radius moyen)
          └── batch-jobs/           # Traitement par lots (Blast Radius faible)
      

      Important : vous devez dans la mesure du possible également prendre en compte la Rate of Change. Cela signifie qu’il faut éviter que des States à fréquence de changement élevée soient référencés par d’autres States qui ne devraient presque jamais changer. Si une ressource est sujette à des modifications régulières toutes les quelques semaines ou mois, elle ne devrait probablement pas occuper un rôle critique dans l’infrastructure, mais plutôt être déplacée dans un Ephemeral State dédié.
      Un taux de changement élevé et une infrastructure critique sont incompatibles - mais c’est à vous de définir où se trouvent les limites, et cette décision doit être prise et documentée le plus tôt possible.
      Nous parlerons plus en détail de la Rate of Change et de sa maîtrise dans un prochain article de cette série.

      Approche 2 : Inversion des dépendances avec Remote State

      Au lieu de créer des dépendances directes, nous désamorçons la situation à l’aide de Data Sources et de préconditions dans le Lifecycle. Laissez-moi vous l’expliquer avec un exemple.

      Le problème : dépendances directes (Anti-Pattern)


       # Anti-Pattern : dépendance directe
      resource "aws_instance" "app" {
        subnet_id = aws_subnet.main.id  
      }
      

      Analysons cela de plus près :

      • L’instance EC2 référence directement une ressource Subnet dans le même State. Cela signifie que les deux ressources sont couplées dans le même fichier terraform.tfstate.
      • Un terraform destroy sur le subnet supprimera les deux ressources en même temps. Cela peut aussi arriver lors d’un terraform apply, si une modification force une suppression et une recréation du subnet, alors l’aws_instance sera également détruite et redéployée.
      • Conclusion : même des modifications sur le subnet peuvent affecter involontairement l’instance EC2.

      Pourquoi cela est dangereux :

      • Blast Radius élevé : une modification du réseau peut détruire l’application, et dans le pire des cas, les données locales stockées dans l’EC2.
      • Cycles de vie couplés : le subnet et l’instance EC2 doivent toujours être gérés ensemble. Un changement sur le réseau nécessite également une demande de changement pour l’application.
      • Conflits de responsabilité : l’équipe réseau et l’équipe applicative peuvent se bloquer mutuellement.
      • Problèmes de rollback : il devient impossible de restaurer l’instance EC2 depuis une sauvegarde, car la configuration réseau du backup ne correspond plus à la situation actuelle.

      Une solution partielle : Dependency Inversion (Best Practice)

      Contre les modifications (ou mutations), nous avons encore peu de pouvoir. Mais nous pouvons nous protéger contre une suppression accidentelle (destroy) en découplant l’instance EC2 des réseaux.


      # App State (consomme les Outputs du Network)
      data "terraform_remote_state" "network" {
        backend = "s3"
        config = {
          bucket = "terraform-states"
          key    = "foundation/network/terraform.tfstate"
        }
      }
      

      Qu’est-ce qui change ici ?

      • La VPC et le subnet sont placés dans un State séparé (foundation/network/).
      • L’application ne référence plus directement le subnet, mais via une Data Source de Remote State.
      • Il n’y a donc plus de dépendance directe ressource-à-ressource entre l’instance EC2 et le subnet.

      L’application dépend encore du subnet, mais de manière abstraite via les outputs du state réseau. Il s’agit donc d’une dépendance indirecte.

      Les dépendances via Remote State ne sont pas une protection totale contre le Blast Radius. Elles agissent plutôt comme un disjoncteur (circuit breaker) vous laissant le temps de réagir, mais les dépendances doivent malgré tout être résolues.

      C’est donc seulement une première étape. En effet, si la ressource réseau dans le Remote State change, cela aura encore un impact sur notre instance EC2. Nous allons donc aller plus loin en implémentant une validation et une programmation défensive.

      La validation et la programmation défensive

      Dans locals.tf, nous définissons :


      locals {
        vpc_id = try(data.terraform_remote_state.network.outputs.vpc_id, null)
        subnet_ids = try(data.terraform_remote_state.network.outputs.subnet_ids, [])
      }

      Que fait ce code ?

      Il implémente un comportement défensif : la fonction try() empêche les erreurs si le Remote State n’est pas disponible. Dans ce cas, vpc_id prend la valeur null, et subnet_ids devient une liste vide (car une VPC peut contenir plusieurs subnets).

      Il existe plusieurs approches possibles à partir de là.

      Option 1 : Detect-Only Pattern

      Nous déclarons maintenant notre instance EC2 avec un Lifecycle :


      resource "aws_instance" "app" {
        lifecycle {
          precondition {
            condition = can(data.terraform_remote_state.network.outputs.vpc_id)
            error_message = "WARNING: Network state not available - using fallback configuration"
          }
        }
      
        subnet_id = try(
          data.terraform_remote_state.network.outputs.private_subnet_ids[0],
          "subnet-fallback-12345"  # Fallback sur un subnet ID stable et connu
        )
      }

      Ce code est volontairement simplifié pour vous faire comprendre le principe de base. En pratique, il peut être encore amélioré, bien sûr. Mais la logique reste similaire :

      • Si aucune VPC n’est définie dans le Remote State (local.vpc_id est alors null), terraform plan échoue avec le message d’erreur défini.
      • Si la VPC existe, mais pas la subnet ID, un fallback est appliqué vers une autre subnet ID. Elle peut être codée en dur (ce qui est moche) ou provenir d’une autre source - ici, c’est le cas le plus simple qui est présenté.

      Dans ce cas précis, cela provoquerait un déplacement du serveur vers un autre subnet, c’est-à-dire une opération de destroy suivie d’un redéploiement. Cela ne nous aide donc pas vraiment avec ce type de ressource. Il peut donc être judicieux de définir également une Lifecycle Precondition pour la subnet ID et de forcer un échec lors de la phase de plan. Ou, en alternative, on peut simplement interdire à Terraform de détruire l’instance EC2, comme nous allons le voir ensuite.

      Option 2 : Prevent Destroy Pattern

      Voici comment empêcher Terraform de détruire une ressource, en provoquant plutôt une erreur explicite :


      resource "aws_instance" "app" {
      
        lifecycle {
          prevent_destroy = true  # Empêche une suppression accidentelle
          precondition {
            condition = can(data.terraform_remote_state.network.outputs.vpc_id)
            error_message = "WARNING: Network state not available - using fallback configuration"
          }
        }
      
        subnet_id = try(
          data.terraform_remote_state.network.outputs.private_subnet_ids[0],
          "subnet-fallback-12345"  # Fallback sur un subnet ID stable et connu
        )
      }

      Seule la ligne prevent_destroy = true a été ajoutée ici, le reste de l’exemple est identique au précédent.

      Il existe aussi une troisième variante, qui peut être combinée avec cette solution.

      Option 3 : Explicit Confirmation Pattern

      Dans ce cas, vous utilisez une variable booléenne qu’il faut passer à true pour approuver explicitement la suppression et la recréation d’une instance.

      Attention : Cela ne protège pas contre une destruction déclenchée par des changements dans le réseau ! Ce mécanisme dans la boucle for_each ne vous protège que de vous-même, en vous forçant à activer manuellement var.confirm_network_dependency_removal pour signifier que vous acceptez ce risque. Utilisez cette approche uniquement si la stabilité de votre pipeline CI/CD est plus importante que l’existence même de l’instance serveur - par exemple, dans des environnements de développement ou sous une stricte politique d’immutabilité.


      variable "confirm_network_dependency_removal" {
        type        = bool
        default     = false
      }
      
      data "terraform_remote_state" "network" {
        backend = "s3"
        config = {
          bucket = "terraform-states"
          key    = "foundation/network/terraform.tfstate"
          region = "eu-central-1"
        }
      }
      
      locals {
        vpc_ok = can(data.terraform_remote_state.network.outputs.vpc_id)
        subnet_ok = can(data.terraform_remote_state.network.outputs.private_subnet_ids[0])
      
        deploy_app = (
          local.vpc_ok || var.confirm_network_dependency_removal
        ) && local.subnet_ok
      
        app_instances = local.deploy_app ? { "main" = true } : {}
      }
      
      resource "aws_instance" "app" {
        for_each = local.app_instances
      
        ami           = "ami-12345678"
        instance_type = "t3.micro"
      
        subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
      
        lifecycle {
          prevent_destroy = true
          precondition {
            condition     = local.vpc_ok
            error_message = "VPC missing – deployment denied"
          }
          precondition {
            condition     = local.subnet_ok
            error_message = "Subnet missing – deployment denied"
          }
        }
      
        tags = {
          Name = "BlastRadiusProtected"
        }
      }
      

      Approche 3 : garde-fous pour le Blast Radius avec des règles de cycle de vie

      Je l’ai déjà évoqué dans les explications de l’approche 2 : Terraform propose plusieurs mécanismes pour empêcher les suppressions accidentelles. 

      Option 1 : prevent_destroy pour les ressources critiques

      Nous l’avons déjà expliqué plus haut, mais rappelons-le à nouveau :


      # Prevent Destroy pour les ressources critiques
      resource "aws_vpc" "main" {
        cidr_block = "10.0.0.0/16"
        lifecycle {
          prevent_destroy = true
        }
      tags = { Name = "Production-VPC" Environment = "production" BlastRadius = "high" }
      }

       

      Observez bien ce que nous faisons ici : nous protégeons les ressources dépendantes comme le sous-réseau et l’instance EC2 en désignant le VPC comme ressource critique et en empêchant sa suppression. Cela n’est bien entendu possible que si le VPC est également géré par nous et ne provient pas d’un Remote State. 

      Option 2 : create_before_destroy pour les ressources stateful

      Dès qu’un provider précise qu’il est impossible de modifier les arguments d’une ressource pendant son fonctionnement, celle-ci doit être détruite puis recréée. Comme de nombreuses ressources dans une infrastructure ne peuvent exister qu’une seule fois (comme les adresses IP, par exemple), Terraform commence par détruire la ressource avant d’en créer une nouvelle.  

      Il est toutefois possible d’autoriser la destruction d’une ressource à condition que la nouvelle soit d’abord provisionnée, avant que l’ancienne ne soit supprimée :


      resource "aws_db_instance" "main" {
      
        lifecycle {
          create_before_destroy = true
          ignore_changes = [
            password,  # Ignorer les modifications de mot de passe pour éviter des remplacements inutiles
          ]
        }

      [...]
      }

      Dans ce cas, c’est à vous de vous assurer qu’aucun conflit n’aura lieu. L’ancienne ressource et la nouvelle doivent pouvoir coexister ; dans le cas d’une adresse IP, qui doit rester unique sur le réseau, il serait judicieux de créer la nouvelle ressource sans cette IP, puis de transférer l’adresse IP de l’ancienne vers la nouvelle. Cela augmente donc parfois la complexité et ne peut être utilisé que de manière sélective.

      Option 3 : destruction conditionnelle avec des préconditions

      La troisième option consiste à utiliser une précondition dans le cycle de vie pour interroger un indicateur (flag) qui détermine si une ressource peut être supprimée ou si une erreur doit être déclenchée et l’exécution arrêtée lors du terraform plan :


      variable "confirmed_destroy" {
        type        = bool
        default     = false
        description = "Permet explicitement la suppression de ressources si défini à `true`."
      }
      
      resource "aws_instance" "app" {
      
        lifecycle {
          precondition {
            condition = var.environment != "production" || var.confirmed_destroy == true
            error_message = "La suppression de ressources en production requiert une confirmation explicite."
          }
        }

      [...]
      }

      Approche 4 : approche professionnelle avec Policy-as-Code (Terraform Enterprise)

      Les solutions précédentes ont un point commun : elles relèvent toutes plus ou moins de bricolages approximatifs et impliquent des compromis. Si l’on recherche une solution professionnelle, il faut se tourner vers Terraform Enterprise.  Pour les environnements d’entreprise, Terraform Enterprise propose, avec Sentinel, des fonctionnalités avancées de Policy-as-Code :


      # Sentinel Policy : contrôle du Blast Radius
      import "tfplan/v2" as tfplan
      
      # Empêcher la suppression de ressources avec un Blast Radius élevé
      high_blast_radius_resources = [
          "aws_vpc",
          "aws_route53_zone", 
          "aws_iam_role"
      ]
      
      main = rule {
          all tfplan.resource_changes as _, resource {
              resource.type not in high_blast_radius_resources or
              resource.change.actions not contains "delete"
          }
      }

      Dans cet exemple, la première étape consiste à désigner les VPC AWS, les rôles IAM et les zones DNS Route 53 comme infrastructures critiques à fort Blast Radius.

      Dans un second temps, toutes les tentatives de suppression de ressources de ces types sont interceptées par une règle spécifique. Si cette règle est déclenchée, Terraform interrompt alors un terraform apply immédiatement après la phase de plan et refuse d’exécuter l’opération.

      Conclusion (provisoire)

      Le Blast Radius dans Terraform doit impérativement être pris en compte dès le départ, sinon on s’expose à un risque important. Ce risque peut être légèrement atténué avec la version gratuite de Terraform à l’aide de quelques astuces et bricolages lors de l’implémentation des modules, mais il ne peut pas être éliminé de manière fiable.  

      Si vous recherchez une solution de niveau professionnel, qui peut également être imposée et auditée à l’échelle de l’entreprise, vous devriez envisager Terraform Enterprise avec le module Sentinel. Si votre gestion des risques est capable d’évaluer le risque et les coûts d’un incident, une licence appropriée pour Terraform Enterprise constitue une forme d’assurance dont le coût peut être mis en perspective avec les avantages attendus. 

       
      Perspectives

      Nous avons maintenant longuement abordé les mesures à prendre pour réduire le risque d’un Blast Radius trop important.

      Mais que faire si cela se produit malgré tout ? Quelles sont les options pour limiter les dégâts et rétablir l’infrastructure dans un état fonctionnel ?

      Nous verrons cela dans la prochaine partie de cette série d’articles.