Dans la dernière partie de cette série, nous avons montré comment des Data Sources apparemment inoffensives dans les modules Terraform peuvent devenir un véritable problème de performance. Des exécutions de terraform plan durant plusieurs minutes, des pipelines instables et des effets incontrôlables de limitation d’API en étaient les conséquences.
Mais comment éviter élégamment et durablement ce piège de mise à l’échelle ?
Dans cette partie, nous présentons des modèles d’architecture éprouvés vous permettant de centraliser les Data Sources, de les injecter avec parcimonie en termes de ressources, et ainsi de garantir des exécutions Terraform rapides, stables et prévisibles, même avec des centaines d’instances de modules.
Au programme : trois stratégies de solution évolutives, un guide pas-à-pas éprouvé en pratique et une checklist de bonnes pratiques pour des modules d’infrastructure prêts pour la production.
Bonnes pratiques : alternatives évolutives
Solution 1 (scénarios simples) : Variable Injection Pattern
Au lieu d’utiliser des Data Sources dans les modules, injectez les données nécessaires sous forme de variables :
data "oci_identity_availability_domains" "available" { compartment_id = var.tenancy_ocid } data "oci_core_subnets" "database" { compartment_id = var.compartment_id vcn_id = var.vcn_id filter { name = "display_name" values = ["*database*"] } } locals { availability_domains = data.oci_identity_availability_domains.available.availability_domains database_subnets = data.oci_core_subnets.database.subnets } module "databases" { for_each = var.database_configs != null ? var.database_configs : {} source = "./modules/database" availability_domains = local.availability_domains subnet_ids = [for subnet in local.database_subnets : subnet.id] name = each.key size = each.value.size }
variable "availability_domains" { type = list(object({ name = string id = string })) description = "Available ADs for database placement" } variable "subnet_ids" { type = list(string) description = "Database subnet IDs" } resource "oci_database_db_system" "main" { for_each = var.db_systems != null ? var.db_systems : {} availability_domain = var.availability_domains[0].name subnet_id = var.subnet_ids[0] compartment_id = var.compartment_id }
Solution 2 (scénarios complexes) : Structured Configuration Pattern
Pour des scénarios plus complexes, utilisez des objets de configuration structurés :
data "oci_core_images" "ol8" { compartment_id = var.tenancy_ocid operating_system = "Oracle Linux" operating_system_version = "8" } locals { compute_images = { "VM.Standard.E4.Flex" = { image_id = [for img in data.oci_core_images.ol8.images : img.id if can(regex(".*E4.*", img.display_name))][0] boot_volume_size = 50 } "BM.Standard3.64" = { image_id = [for img in data.oci_core_images.ol8.images : img.id if can(regex(".*Standard.*", img.display_name))][0] boot_volume_size = 100 } } network_config = { availability_domains = data.oci_identity_availability_domains.ads.availability_domains vcn_id = data.oci_core_vcn.main.id } } module "compute_instances" { for_each = var.instance_configs != null ? var.instance_configs : {} source = "./modules/compute-instance" compute_config = local.compute_images[each.value.shape] network_config = local.network_config }
variable "compute_config" { type = object({ image_id = string boot_volume_size = number }) description = "Pre-resolved compute configuration" } variable "network_config" { type = object({ availability_domains = list(object({ name = string id = string })) vcn_id = string }) description = "Pre-resolved network configuration" } resource "oci_core_instance" "this" { for_each = var.instances != null ? var.instances : {} availability_domain = var.network_config.availability_domains[0].name compartment_id = var.compartment_id source_details { source_id = var.compute_config.image_id source_type = "image" boot_volume_size_in_gbs = var.compute_config.boot_volume_size } }
Solution 3 (scénarios très complexes) : Data Proxy Pattern
Pour les scénarios très complexes, créez des modules "Data Proxy" dédiés :
data "oci_core_images" "oracle_linux" { compartment_id = var.tenancy_ocid operating_system = "Oracle Linux" operating_system_version = "8" } data "oci_core_vcn" "main" { vcn_id = var.vcn_id } data "oci_core_security_lists" "web" { compartment_id = var.compartment_id vcn_id = var.vcn_id filter { name = "display_name" values = ["*web*"] } } output "platform_data" { value = { image_id = data.oci_core_images.oracle_linux.images[0].id vcn_id = data.oci_core_vcn.main.id instance_shapes = { small = "VM.Standard.E3.Flex" medium = "VM.Standard.E4.Flex" large = "VM.Standard3.Flex" } } }
module "platform_data" { source = "./modules/data-proxy" tenancy_ocid = var.tenancy_ocid compartment_id = var.compartment_id vcn_id = var.vcn_id } module "web_servers" { for_each = var.web_server_configs != null ? var.web_server_configs : {} source = "./modules/oci-instance" platform_data = module.platform_data.platform_data name = each.key instance_type = each.value.size }
Comparaison des performances
Un exemple concret issu d’un projet client avec le déploiement de 50 instances VM montre la différence spectaculaire :
|
Après : Variable Injection |
|
|
150 appels API | 3 appels API |
|
$ time terraform plan real 4m23.415s |
$ time terraform plan real 0m18.732s |
Résultat : 93 % de temps de planification en moins et 98 % de réduction des appels API.
Variable Injection : guide étape par étape
Étape 1 : Centraliser les Data Sources
Objectif : Supprimer toutes les Data Sources des modules et les centraliser dans le module racine afin de consolider les appels API et créer une source de vérité unique.
Comment : Déplacez toutes les Data Sources utilisées par les modules dans le module racine. Cela garantit que chaque information n’est récupérée qu’une seule fois, quel que soit le nombre de modules qui en ont besoin. Vous réduisez ainsi le nombre d'appels API de N×M (nombre de modules × nombre de Data Sources) à M (nombre de Data Sources).
data "oci_identity_availability_domains" "ads" { compartment_id = var.tenancy_ocid } data "oci_core_images" "ol8" { compartment_id = var.tenancy_ocid operating_system = "Oracle Linux" operating_system_version = "8" } data "oci_core_vcn" "main" { vcn_id = var.vcn_id }
Étape 2 : Traiter les données dans des locals
Objectif : Transformer les résultats bruts des Data Sources en un format exploitable, tout en gardant la complexité hors des modules.
Comment : Utilisez des locals pour filtrer, trier et transformer les résultats des Data Sources en structures de données. Cela permet de gérer la logique complexe de manière centralisée et d’alimenter les modules avec des données déjà nettoyées et structurées. Grâce aux boucles for et aux expressions conditionnelles, vous pouvez également implémenter des mécanismes de secours et des logiques de validation.
locals { availability_domains = [ for ad in data.oci_identity_availability_domains.ads.availability_domains : ad.name ] compute_images = { standard = [ for img in data.oci_core_images.ol8.images : img.id if can(regex(".*Standard.*", img.display_name)) ][0] gpu = [ for img in data.oci_core_images.ol8.images : img.id if can(regex(".*GPU.*", img.display_name)) ][0] } network_config = { vcn_id = data.oci_core_vcn.main.id vcn_cidr = data.oci_core_vcn.main.cidr_block availability_domains = local.availability_domains } }
Étape 3 : Définir des variables dans les modules
Objectif : Créer des interfaces claires pour la transmission de données aux modules tout en assurant la validation et la sécurité des types.
Comment : Remplacez les Data Sources dans les modules par des variables typées avec des descriptions explicites et des règles de validation. Les définitions de type assurent la cohérence et la robustesse, tandis que les blocs de validation garantissent que seules des données valides sont transmises aux modules. Cela rend les modules plus testables et indépendants de l’API du fournisseur cloud.
variable "availability_domains" { type = list(string) description = "List of available availability domains" validation { condition = length(var.availability_domains) > 0 error_message = "At least one availability domain must be provided." } } variable "compute_images" { type = map(string) description = "Map of compute images by type" validation { condition = alltrue([ for image_id in values(var.compute_images) : can(regex("^ocid1\\.image\\.", image_id)) ]) error_message = "All image IDs must be valid OCI OCIDs." } }
Étape 4 : Implémenter les modules sans Data Sources
Objectif : Libérer complètement les modules des appels API externes et les transformer en conteneurs de définitions de ressources pures.
Comment : Remplacez toutes les références aux Data Sources dans les modules par des références de variables. Cela rend les modules déterministes et prévisibles, car ils ne travaillent qu’avec les paramètres fournis et n’exécutent plus d’appels API imprévus. Les modules deviennent ainsi testables de manière indépendante grâce à l’injection de données simulées via les variables.
resource "oci_core_instance" "this" { for_each = var.instances != null ? var.instances : {} availability_domain = var.availability_domains[each.value.ad_index] compartment_id = var.compartment_id shape = each.value.shape create_vnic_details { subnet_id = each.value.subnet_id } source_details { source_id = var.compute_images[each.value.image_type] source_type = "image" } metadata = { ssh_authorized_keys = var.ssh_public_key } }
Étape 5 : Appeler les modules avec des données injectées
Objectif : Établir la liaison entre les données collectées de manière centralisée et les modules, afin de mettre en place un modèle de flux de données clair.
Comment : Transmettez les données traitées dans les locals comme paramètres aux modules. Cela boucle le processus de Variable Injection : les données sont récupérées une seule fois de manière centralisée, transformées, puis réparties de manière ciblée aux modules. Cette transmission explicite crée des dépendances claires, compréhensibles à la fois pour les humains et pour Terraform.
module "web_servers" { for_each = var.web_server_configs != null ? var.web_server_configs : {} source = "./modules/compute" availability_domains = local.availability_domains compute_images = local.compute_images network_config = local.network_config instances = each.value.instances compartment_id = each.value.compartment_id ssh_public_key = var.ssh_public_key }
Checklist de bonnes pratiques
✅ À faire : patterns évolutifs
- [ ] Data Sources centralisées : définir toutes les Data Sources dans le root module
- [ ] Variable Injection : transmettre les données aux modules via des variables
- [ ] Objets structurés : organiser les données complexes en objets typés
- [ ] Validation Rules : implémenter des validations pour les variables injectées
- [ ] Documentation : ajouter des descriptions aux variables injectées
- [ ] Traitement local : transformer les données dans les locals du root module
- [ ] Data Proxy Pattern : utiliser des modules de données séparés pour les scénarios très complexes
❌ À éviter : anti-patterns
- [ ] Data Sources dans les modules : ne jamais utiliser de Data Sources dans des modules réutilisables
- [ ] Lookups redondants : Data Sources identiques dans plusieurs modules
- [ ] Filtrage complexe : opérations de filtrage coûteuses dans chaque module
- [ ] Data Sources imbriquées : Data Sources dépendantes d’autres Data Sources
- [ ] Références dynamiques : boucles for_each sur des résultats de Data Source dans les modules
- [ ] Validation manquante : utilisation de données injectées sans validation
Monitoring et débogage
Pour surveiller les performances des Data Sources, vous pouvez analyser la sortie de débogage de terraform plan à la recherche d’entrées liées aux Data Sources :
export TF_LOG=DEBUG export TF_LOG_PATH=./terraform.log terraform plan 2>&1 | grep -E "(data\.|GET|POST)" | wc -l terraform plan 2>&1 | grep -E "data\." | awk '{print $2}' | sort | uniq -c
Conclusion : des modules performants grâce à une architecture réfléchie
Les Data Sources sont une fonctionnalité puissante de Terraform - mais utilisées dans les modules, elles peuvent nuire aux performances. Le Variable Injection Pattern constitue une solution élégante :
Avantages :
- Réduction drastique des appels API (plus de 95 % d’économie possible)
- Mise à l’échelle linéaire des performances au lieu d’une dégradation exponentielle
- Logique de données centralisée pour une meilleure maintenabilité
- Dépendances explicites au lieu d’appels cachés à des Data Sources
- Meilleure testabilité grâce à l’injection de données fictives
La clé réside dans un changement de paradigme : au lieu de récupérer les données lorsqu’elles sont nécessaires, vous les collectez une fois de manière centralisée et les distribuez de manière ciblée.
Chez ICT.technology, nous avons réduit les temps de planification Terraform de plusieurs minutes à quelques secondes grâce à l’application systématique de ces patterns - même avec des centaines d’instances de modules.