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

      Terraform @ Scale - Partie 4a : Les Data Sources sont dangereuses !

      Les Data Sources de Terraform sont un moyen populaire de renseigner dynamiquement des variables avec des valeurs réellement existantes de l’environnement cloud concerné. Mais leur utilisation dans des infrastructures dynamiques demande une certaine prévoyance. Il suffit par exemple d’un innocent data.oci_identity_availability_domains dans un module - et soudain, chaque exécution de terraform plan prend des minutes au lieu de secondes. Car 100 instances de module signifient 100 appels API, et votre fournisseur cloud commence à appliquer un throttling. Bienvenue dans le monde de l’amplification API involontaire via les Data Sources.

      Dans cet article, je vous montre pourquoi les Data Sources dans les modules Terraform peuvent poser un problème de mise à l’échelle.

       

      Le problème de mise à l’échelle caché

      Terraform at scale 11 5

      Scénario : Le piège des 10 secondes

      Vous avez écrit un module Terraform propre pour les instances VM. Pour chaque VM, vous avez besoin d’une Availability Domain, vous utilisez donc une Data Source :


      data "oci_identity_availability_domains" "ads" {
        compartment_id = var.tenancy_ocid
      }
      
      resource "oci_core_instance" "this" {
        for_each = var.instances != null ? var.instances : {}
      
        availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
        compartment_id      = var.compartment_id
        shape              = each.value.shape
        
        create_vnic_details {
          subnet_id = each.value.subnet_id
        }
      }

      À première vue, tout est correct ici. Le module fonctionne parfaitement - pour une seule instance VM, voire même pour quelques-unes.

      Mais ensuite, votre client passe à 50 VMs :


      module "app_servers" {
        for_each = var.server_configs != null ? var.server_configs : {}
        
        source = "./modules/compute-instance"
        
        instances      = each.value.instances
        compartment_id = each.value.compartment_id
      }

       Le résultat : 50 appels API identiques vers oci_identity_availability_domains à chaque terraform plan et terraform apply.

      Ce qui prenait auparavant quelques secondes dure maintenant plusieurs minutes. Avec 100 ou 200 instances, cela devient réellement douloureux.

      Pour des instances VM nouvellement provisionnées, cela peut encore être tolérable et planifiable dans de nombreux cas d’usage - mais que se passe-t-il s’il s’agit de ressources existantes avec des cycles de vie courts ? Imaginez des temps d’attente plus longs lors du provisionnement de nœuds de cluster pour absorber des pics de charge, de backends de Load Balancer, d’enregistrements DNS ou même de ressources dont dépendent d’autres ressources. Dans ce cas, de longues interruptions peuvent survenir, avec des répercussions visibles pour vos clients - dans le pire des cas, de manière imprévisible.

      Pourquoi les Data Sources posent problème dans les modules

      Les Data Sources se comportent fondamentalement différemment des ressources. Elles sont exécutées à chaque terraform plan et terraform apply, et ne peuvent pas être mises en cache. Dans un module, cela signifie :

      Problème 1 : Amplification API

      Chaque instance de module exécute ses propres requêtes Data Source, même si les données sont identiques.

      Par exemple, ces deux modules qui appellent tous deux le même sous-module ./modules/server :


      module "web_servers" {
        for_each = var.web_configs != null ? var.web_configs : {}
        source   = "./modules/server"
        
        compartment_id = each.value.compartment_id
      }
      
      module "app_servers" {
        for_each = var.app_configs != null ? var.app_configs : {}
        source   = "./modules/server"
        
        compartment_id = each.value.compartment_id
      }

      Dans le module ./modules/server se cache, de manière invisible pour l’auteur du module racine, une requête vers une Data Source :


      data "oci_identity_availability_domains" "available" {
        compartment_id = var.compartment_id
      }
      
      resource "oci_core_instance" "this" {
        for_each = var.instances != null ? var.instances : {}
        
        availability_domain = data.oci_identity_availability_domains.available.availability_domains[0].name
        compartment_id      = var.compartment_id
      }

       Si, par exemple, 10 app_servers et 50 web_servers sont déployés, cela entraîne 60 requêtes API parallèles (!). Aucun fournisseur cloud ne permet cela. À la place, votre terraform apply se heurte aux limites API du gateway, et le déploiement devient extrêmement lent.

      Dans le meilleur des cas, les serveurs sont déployés par petits lots, par exemple en paquets de 10 instances chacun. La durée de déploiement est alors déjà multipliée au moins par six.

      Et cela ne concerne pas uniquement les instances de serveur présentées dans l’exemple de code, mais aussi toutes les autres ressources encore présentes dans le plan. La limite API s’applique à tout, pas uniquement à un type de ressource spécifique comme oci_core_instance. D’autres ressources, normalement accédées de manière synchrone ou parallèle, doivent maintenant attendre leur tour. Cela peut, dans le pire des cas, conduire à des conditions de concurrence (Race Conditions) ou des délais d’expiration (Timeouts).

      Problème 2 : Dégradation des performances

      Lorsque le nombre d’instances de module augmente, le temps de planification croît de manière linéaire, voire exponentielle.

      Jetez un œil à cet exemple, qui lit la liste des images OS existantes dans OCI et filtre pour une version spécifique optimisée GPU de Oracle Linux 8 :


      data "oci_core_images" "compute_images" {
        compartment_id           = var.compartment_id
        operating_system         = "Oracle Linux"
        operating_system_version = "8"
        sort_by                  = "TIMECREATED"
        sort_order              = "DESC"
        
        filter {
          name   = "display_name"
          values = [".*GPU.*"]
          regex  = true
        }
      }
      
      resource "oci_core_instance" "gpu_instances" {
        for_each = var.gpu_instances != null ? var.gpu_instances : {}
        
        source_details {
          source_id   = data.oci_core_images.compute_images.images[0].id
          source_type = "image"
        }
      }

       La liste des images OS existantes est extrêmement longue et elle est lue intégralement à chaque fois, pour chaque instance VM, puis filtrée par le provider Terraform. Cela entraîne donc non seulement des appels API inutiles avec des réponses très volumineuses, mais surcharge également Terraform lui-même.

      Problème 3 : Limites API et throttling

      Comme mentionné plus haut dans cet article : les fournisseurs cloud imposent des limites API.

      Trop de requêtes Data Source entraînent donc :

      • des erreurs HTTP 429 (Too Many Requests)
      • des cycles d’attente et de nouvelle tentative (Exponential Backoff et Retry)
      • le blocage d’autres opérations Terraform
      • des pipelines CI/CD instables

       

      Anti-Pattern : Exemples non scalables

      Terraform at scale 11 4

      Exemple 1 : Le dilemme des Availability Domains

      Problème : Ce module exécute deux appels API distincts à chaque instanciation - un pour les Availability Domains et un autre pour les Subnets. Dans un environnement avec 20 modules de base de données, cela représente 40 appels API redondants pour des informations identiques.

      Pourquoi ce n’est pas scalable : Chaque module interroge les mêmes Availability Domains globaux, alors qu’ils changent très rarement. De plus, à chaque instance de module, le même filtre de Subnet est exécuté, ce qui devient particulièrement coûteux dans les VPC complexes avec de nombreux subnets.

      Conséquence : Pour 50 instances de base de données = 100 appels API + 50 filtrages pour des données qui changent au maximum une fois par an.


      data "oci_identity_availability_domains" "available" {
        compartment_id = var.compartment_id
      }
      
      data "oci_core_subnets" "database" {
        compartment_id = var.compartment_id
        vcn_id         = var.vcn_id
        
        filter {
          name   = "display_name"
          values = ["*database*"]
        }
      }
      
      resource "oci_database_db_system" "main" {
        for_each = var.db_systems != null ? var.db_systems : {}
        
        availability_domain = data.oci_identity_availability_domains.available.availability_domains[0].name
        subnet_id          = data.oci_core_subnets.database.subnets[0].id
        compartment_id     = var.compartment_id
      }

       

      Exemple 2 : Le problème du lookup d’image

      Problème : Cette implémentation effectue une recherche d’image coûteuse avec filtrage Regex à chaque appel de module. Le tri et le traitement local des listes d’images est très gourmand en ressources, à la fois côté OCI et dans Terraform.

      Pourquoi ce n’est pas scalable : Les recherches d’images sont particulièrement lentes, car elles transfèrent de grandes quantités de données et appliquent des filtres complexes. Chaque VM effectue la même recherche, alors que le résultat est identique pour toutes les instances. Le tri local avec sort() aggrave encore le problème.

      Conséquence : Pour 100 instances de VM = 100 appels API d’image coûteux + 100 filtrages locaux = plusieurs minutes de temps de planification pour quelque chose qui pourrait être fait en un clin d'œil.


       data "oci_core_images" "ol8_images" {
        compartment_id           = var.tenancy_ocid
        operating_system         = "Oracle Linux"
        operating_system_version = "8"
        shape                    = var.instance_shape
      
      filter {
      name   = "display\_name"
      values = \["Oracle-Linux-8.8-.\*"]
      regex  = true
      }
      }
      
      locals {
      latest\_image\_id = sort(\[for img in data.oci\_core\_images.ol8\_images.images : img.id])\[0]
      }
      
      resource "oci\_core\_instance" "this" {
      for\_each = var.instances != null ? var.instances : {}
      
      source\_details {
      source\_id   = local.latest\_image\_id
      source\_type = "image"
      }
      } 

       

      Exemple 3 : Le problème des Data Sources imbriquées

      Problème : Ce module cumule plusieurs problèmes de mise à l’échelle : recherches de clusters, lookups d’images pour les worker-nodes, et chaînes de dépendances complexes entre Data Sources. Chaque création de Node Pool relance les deux Data Sources.

      Pourquoi ce n’est pas scalable : Les Data Sources imbriquées génèrent des chaînes de dépendances qui doivent être entièrement parcourues pour chaque instance de module. Les recherches d’images spécifiques à Kubernetes sont particulièrement lentes, car elles utilisent des filtres très spécifiques avec des patterns Regex qui doivent être traités côté serveur.

      Conséquence : Pour 10 Node Pools = 20 appels API pour les informations de cluster + 10 lookups OKE coûteux = des temps de planification exponentiels à mesure que le nombre de Node Pools augmente.


      data "oci_containerengine_clusters" "existing" {
        compartment_id = var.compartment_id
        
        filter {
          name   = "name"
          values = [var.cluster_name]
        }
      }
      
      data "oci_core_images" "worker_images" {
        compartment_id   = var.compartment_id
        operating_system = "Oracle Linux"
        
        filter {
          name   = "display_name"
          values = ["Oracle-Linux-.*-OKE-.*"]
          regex  = true
        }
      }
      
      resource "oci_containerengine_node_pool" "workers" {
        for_each = var.node_pools != null ? var.node_pools : {}
        
        cluster_id = data.oci_containerengine_clusters.existing.clusters[0].id
        
        node_config_details {
          size = each.value.size
        }
      }

       

      Nous vous avons maintenant présenté plusieurs exemples de la manière à ne pas utiliser les Data Sources. Mais cela ne suffit pas à vous aider. Dans le prochain article, je vous montrerai comment éviter élégamment ce piège grâce à une architecture intelligente et à l’injection de variables.

      Car la meilleure Data Source est celle qui n’est pas exécutée - ou exécutée une seule fois.