Aller au contenu
Infrastructure as Code medium

Anti-patterns des modules Terraform : les erreurs à éviter

35 min de lecture

logo terraform

Un anti-pattern de module se reconnaît rarement à sa tête : le code compile, le plan passe, l'infrastructure sort. Il se reconnaît à son symptôme, et celui-ci arrive toujours tard, le jour du deuxième appel, du destroy, ou de la reprise par quelqu'un d'autre. Ce guide part donc du symptôme, mesuré, et remonte à la correction.

Tout ce qui suit a été exécuté sur Terraform v1.15.4 : messages d'erreur, avertissements, résumés de plan et contenu du state compris. Deux affirmations répandues n'y survivent pas, celle du provider embarqué que Terraform accepterait sans rien dire, et celle du bloc moved qui corrigerait presque tout.

  • Le symptôme de chaque anti-pattern, et à quel moment il tombe
  • Pourquoi un provider embarqué n'est pas qu'une gêne de style
  • Comment refactorer une configuration déjà appliquée sans rien détruire
  • Ce que moved ne corrigera jamais, malgré sa réputation
  • L'arbre profond, l'anti-pattern le mieux documenté et le moins nommé

Un module qui fait tout ne se réutilise nulle part : réseau, machines, stockage et DNS dans un seul bloc, avec quarante variables pour en piloter les variantes. Le symptôme n'est pas une erreur, c'est une fourche : chaque nouveau cas ajoute une variable, chaque variable ajoute une condition.

La documentation ne fixe aucun seuil chiffré, et méfiez-vous des articles qui en donnent un. Le critère officiel est qualitatif : « A good module should raise the level of abstraction by describing a new concept in your architecture ».

Voici l'anti-pattern dont on lit le plus de choses fausses. Non, Terraform ne l'accepte pas en silence, et non, la correction ne se réduit pas à « supprimer le bloc ».

Ce que la mesure donne, sur trois cas distincts :

SituationCe que fait Terraform
bloc provider vide dans le moduleWarning: Redundant empty provider block
bloc configuré, appel sans count ni for_eachrien, aucun diagnostic
bloc configuré, appel avec count, for_each ou depends_onerreur immédiate
Error: Module is incompatible with count, for_each, and depends_on
The module at module.cle is a legacy module which contains its own local
provider configurations, and so calls to it may not use the count, for_each,
or depends_on arguments.

Le motif de l'interdiction est le cycle de vie, pas le confort, et la documentation le dit en une phrase : « A provider configuration must always stay present in the overall Terraform configuration for longer than all of the resources it manages. »

La correction complète tient en deux déclarations, l'appelant fournissant la configuration que le module déclare attendre :

# dans le module
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = ">= 2.5"
configuration_aliases = [local.plaques]
}
}
}
# chez l'appelant
module "plaque" {
source = "../bibliotheque/plaque"
providers = {
local.plaques = local.atelier
}
}

3. La contrainte de version trop serrée dans un module partagé

Section intitulée « 3. La contrainte de version trop serrée dans un module partagé »

Un module partagé qui écrit version = "~> 0.8" impose sa borne haute à tous ses consommateurs. La règle officielle est l'inverse : « If you are writing a shared Terraform module, constrain only the minimum required provider version using a >= constraint. »

Le symptôme se mesure, et il est brutal :

- Finding hashicorp/local versions matching "~> 2.4.0, >= 2.9.0"...
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/local: no available releases match the given constraints ~> 2.4.0,
>= 2.9.0

Les contraintes du module et de la racine s'intersectent. Le consommateur est bloqué, et seul un nouveau tag du module le débloque.

4. Le copier-coller, et le refactoring qui détruit

Section intitulée « 4. Le copier-coller, et le refactoring qui détruit »

Deux blocs presque identiques, puis trois : c'est l'anti-pattern le plus banal. Sa correction, elle, est piégeuse, parce que Terraform ne suit pas des ressources mais des adresses. Extraire local_file.plaque_nord vers module.plaque["nord"].local_file.plaque change l'adresse, donc :

Plan: 4 to add, 0 to change, 4 to destroy.

Le bloc moved déclare qu'une adresse en remplace une autre, et le plan change de nature :

moved {
from = local_file.plaque_nord
to = module.plaque["nord"].local_file.plaque
}
# local_file.plaque_nord has moved to module.plaque["nord"].local_file.plaque
Plan: 0 to add, 0 to change, 0 to destroy.

Un détail décide de la fiabilité de votre contrôle : le témoin que vous regardez. L'id d'un local_file est un hachage de son contenu, donc identique après une destruction suivie d'une recréation. Un random_pet, lui, est tiré au hasard : s'il change, la ressource a bien été recréée. Choisissez toujours comme témoin une valeur que l'outil ne sait pas recalculer.

L'erreur symétrique de la précédente. Un moved traite des adresses, et rien d'autre :

Défautmoved le corrige ?
ressource déplacée, renommée, passée dans un moduleoui
ressource gérée à transformer en source de donnéesnon, la doc l'interdit explicitement
variable non typée, valeur figée dans le modulenon, aucun rapport avec l'adressage
bloc provider embarquénon, et son retrait a son propre ordre

La documentation est nette sur le deuxième cas, et c'est la limite la plus mal connue du bloc : « You cannot use the moved block to change a managed resource (a resource block) into a data resource. »

Une variable sans type accepte any. L'erreur d'appel ne se voit alors qu'au moment où la valeur est utilisée, parfois jamais. Un objet explicite en fait un contrat :

variable "plaque" {
type = object({
etiquette = string
intitule = string
})
description = "Etiquette de la plaque et intitule a y inscrire."
}

Le bénéfice défensif est le plus visible, mais ce n'est pas le principal. Un type d'objet décrit un concept, donc rend le module substituable : la doc montre qu'un module de dns_records typé peut être remplacé par une autre implémentation sans que ses appelants changent.

La description fait partie du même contrat. La Standard Module Structure demande que « all variables and outputs should have one or two sentence descriptions », et c'est la seule chose que l'outillage sache extraire.

C'est l'anti-pattern le mieux documenté, et le moins nommé : un module qui appelle un module qui appelle un module. La recommandation officielle est franche, « in most cases we strongly recommend keeping the module tree flat, with only one level of child modules ».

module "reseau" {
source = "./modules/reseau"
}
module "machine" {
source = "./modules/machine"
reseau_id = module.reseau.id
}

Le coût d'un arbre profond est double : il faut descendre pour comprendre ce qui est réellement créé, et toute entrée nouvelle doit être relayée à chaque étage, qui n'en fait rien d'autre que la transmettre.

Cause commune de plusieurs points ci-dessus, et jamais nommée dans la plupart des listes : un module qui crée ou interroge lui-même ce dont il dépend impose ses choix. La doc appelle sa correction l'inversion de dépendance : « Instead of a module embedding its dependencies [...] the module receives its dependencies from the root module. »

Le cas particulier le plus fréquent est la création conditionnelle : « Rather than trying to write a module that itself tries to detect whether something exists and create it if not, we recommend applying the dependency inversion approach ». L'appelant décide, le module reçoit l'objet.

Supprimer un output casse tous les appelants qui le lisent, sans préavis. Depuis la 1.15, il existe un chemin officiel de retrait progressif : l'argument deprecated d'un bloc output, « only valid in child modules ».

output "ancien_chemin" {
value = local_file.plaque.filename
deprecated = "Utilisez la sortie chemin, celle-ci disparaitra en 2.0.0."
}

Le consommateur reçoit un avertissement au plan, et dispose du temps d'une version majeure pour migrer.

Anti-patternSymptômeCorrection
module Godchaque cas ajoute une variabledécouper par concept, tester le nom
provider embarquéModule is incompatible with count, for_each...configuration_aliases plus providers
contrainte trop serréeno available releases match the given constraintsne contraindre que le minimum
copier-collertrois blocs jumeauxun module, un appel, for_each
refactoring destructeurPlan: 4 to add, 4 to destroydes blocs moved
interface anyl'erreur tombe à l'usage, ou jamaisun objet typé et décrit
arbre profondune entrée à relayer sur trois étagesaplatir, relier par expressions
dépendances fabriquéesimpossible de réutiliser un existantinversion de dépendance
sortie suppriméeles appelants cassent sans préavisdeprecated puis retrait

Le lab anti-patterns des modules remet un projet déjà appliqué, fait de deux ressources copiées-collées. Il faut les extraire dans un module typé appelé une seule fois, sans qu'aucune ne soit détruite. Le témoin du contrôle est un jeton random_pet, précisément parce qu'il ne se recalcule pas : le même refactoring sans blocs moved détruit et recrée les quatre ressources, et le lab le voit. Il se joue hors ligne.

  • Un anti-pattern se reconnaît à son symptôme, qui arrive au pire moment.
  • Le provider embarqué n'est pas silencieux : avertissement s'il est vide, erreur dès qu'on veut count ou for_each.
  • Sa correction complète, c'est configuration_aliases et providers, dans cet ordre sur une infrastructure appliquée.
  • Un module partagé ne contraint que son minimum : une borne haute bloque ses consommateurs.
  • Terraform suit des adresses : sans moved, un refactoring détruit.
  • Un moved ne corrige que des adresses, jamais un type ni un provider.
  • Choisissez comme témoin de non-destruction une valeur non recalculable.
  • L'arbre des modules reste plat, et un module reçoit ses dépendances.
  • Une sortie se retire avec deprecated, jamais d'un coup.

Les questions ci-dessous portent sur ce qui casse en pratique : le module qu'on ne peut pas appeler deux fois, le refactoring qui détruit, et le moved auquel on prête trop de pouvoirs.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn