
Une configuration Terraform doit s'adapter : 4 Go en production, 1 Go en développement. Elle doit aussi refuser ce qui n'a pas de sens, plutôt que de le provisionner et de le découvrir trop tard.
Ces deux besoins relèvent de mécanismes différents, souvent confondus. Le premier est une expression conditionnelle, qui choisit une valeur. Le second relève des conditions personnalisées, qui posent une garantie et arrêtent Terraform quand elle est rompue. Ce guide traite les deux, et surtout ce qui les sépare.
Tous les exemples ont été exécutés sur Terraform v1.15.4 avant publication,
résultats et messages compris. Ils n'utilisent que le provider random : vous
pouvez les rejouer sans aucune infrastructure.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- L'expression conditionnelle
condition ? a : b, et pourquoi ce n'est pas un opérateur - La conversion automatique des branches, et pourquoi la doc conseille de s'en méfier
- La priorité des opérateurs, absente de la plupart des tutoriels
- Les quatre niveaux de conditions :
validation,precondition,postcondition, bloccheck - Lire le verdict de toutes vos conditions en une seule commande
Prérequis
Section intitulée « Prérequis »- Variables et locals connus (variables Terraform)
- Notions d'expressions HCL (expressions Terraform)
- Terraform 1.2 pour
preconditionetpostcondition, 1.5 pour les blocscheck
L'expression conditionnelle
Section intitulée « L'expression conditionnelle »La forme est familière : condition ? valeur_si_vrai : valeur_si_faux.
locals { memoire = var.environnement == "prod" ? 4096 : 1024}Un point de vocabulaire qui compte quand on cherche dans la documentation :
? : n'est pas un opérateur. La documentation officielle est explicite, le
caractère ? combiné au : fait partie d'une expression conditionnelle et
n'est pas considéré comme un opérateur. Cherchez « conditional expression », pas
« ternary operator ».
La conversion automatique des branches, et son piège
Section intitulée « La conversion automatique des branches, et son piège »Les deux branches n'ont pas besoin d'être du même type. Terraform les convertit vers un type commun, ce qui est plus permissif que ce qu'annoncent beaucoup de tutoriels :
echo 'true ? 12 : "hello"' | terraform console"12"Le nombre 12 est devenu la chaîne "12", parce que le type commun aux
deux branches est string. Aucune erreur, aucun avertissement.
C'est précisément pourquoi la documentation recommande de ne pas s'appuyer sur ce comportement : elle le qualifie de source de confusion et conseille d'expliciter avec les fonctions de conversion.
locals { # Explicite : on lit immediatement que le resultat est une chaine taille = var.actif ? tostring(12) : "hello"}Ternaires imbriqués et associativité
Section intitulée « Ternaires imbriqués et associativité »Pour un if / else if / else, on imbrique :
locals { vcpu = var.env == "prod" ? 4 : var.env == "staging" ? 2 : 1}Cette écriture fonctionne : pour staging, elle rend bien 2, l'évaluation se
faisant de droite à gauche. Une réserve d'honnêteté cependant : cette
associativité n'est documentée ni sur la page des expressions conditionnelles ni
sur celle des opérateurs. Elle est vraie en pratique sur 1.15.4, mais elle n'est
pas garantie par écrit. Au delà de deux niveaux, parenthésez : le gain de
lisibilité vaut mieux qu'un comportement non documenté.
locals { vcpu = var.env == "prod" ? 4 : (var.env == "staging" ? 2 : 1)}La priorité des opérateurs
Section intitulée « La priorité des opérateurs »Point systématiquement omis, et source de bugs silencieux. Du plus prioritaire au moins prioritaire :
| Rang | Opérateurs |
|---|---|
| 1 | ! et - unaire |
| 2 | *, /, % |
| 3 | +, - |
| 4 | >, >=, <, <= |
| 5 | ==, != |
| 6 | && |
| 7 | || |
Conséquence directe : && lie plus fort que ||.
echo 'true || false && false' | terraform console # trueecho '(true || false) && false' | terraform console # falseLa première expression se lit true || (false && false), donc true. Si vous
vouliez l'autre lecture, seules les parenthèses l'imposent.
Concernant l'évaluation paresseuse de && et ||, elle existe en pratique
sur 1.15.4, mais la documentation ne la mentionne pas. Ne bâtissez donc pas une
protection dessus : écrivez la garde explicitement plutôt que de compter sur le
fait que la seconde branche ne sera pas évaluée.
Le piège de l'égalité sur les collections
Section intitulée « Le piège de l'égalité sur les collections »L'égalité est stricte sur les types : "512" == 512 vaut false. Jusque là,
rien de surprenant. Le cas qui piège vraiment concerne les collections
vides.
variable "vide" { type = list(string) default = []}| Expression | Résultat |
|---|---|
var.vide == [] | false |
var.vide == tolist([]) | false |
length(var.vide) == 0 | true |
Le littéral [] construit un tuple vide, pas une list(string). La
comparaison échoue donc sur le type, même quand la variable est effectivement
vide. Testez toujours la longueur :
locals { aucun_service = length(var.services) == 0}Les quatre niveaux de conditions personnalisées
Section intitulée « Les quatre niveaux de conditions personnalisées »Choisir une valeur est une chose, garantir une propriété en est une autre. Terraform offre quatre mécanismes, qui diffèrent par le moment où ils s'évaluent et par ce qu'ils bloquent.
| Mécanisme | Où il vit | Quand il s'évalue | Effet en cas d'échec |
|---|---|---|---|
validation | bloc variable | à l'évaluation de la variable | arrête le plan |
precondition | lifecycle d'une ressource, ou bloc output | avant l'action | arrête avant de créer |
postcondition | lifecycle d'une ressource | après l'action | arrête après création |
bloc check | au premier niveau | après l'apply | avertit seulement |
validation : refuser une entrée invalide
Section intitulée « validation : refuser une entrée invalide »variable "environnement" { type = string
validation { condition = contains(["dev", "staging", "prod"], var.environnement) error_message = "L'environnement doit valoir dev, staging ou prod." }}Une idée fausse circule à propos de ces conditions : elles seraient limitées à
des fonctions « pures », sans accès au disque ni au réseau. C'est inexact.
Vérification sur 1.15.4, la condition suivante est acceptée, et file() lit bel
et bien le disque :
validation { condition = can(file("${path.module}/data.txt")) && startswith(var.nom, "lab") error_message = "Fichier requis absent, ou prefixe invalide."}Depuis Terraform 1.9, une condition de validation peut en outre référencer d'autres variables et d'autres objets, y compris des data sources, donc des valeurs obtenues par le réseau.
Un point à connaître en revanche : une validation qui référence une autre
variable n'est pas attrapée par terraform validate, qui rend valid: true.
L'erreur ne tombe qu'au plan.
precondition : vérifier avant d'agir
Section intitulée « precondition : vérifier avant d'agir »La precondition vit dans le bloc lifecycle d'une ressource. Elle s'évalue
avant que Terraform ne crée quoi que ce soit, ce qui évite une création
partielle.
resource "random_pet" "service" { length = var.longueur
lifecycle { precondition { condition = var.longueur <= 5 error_message = "Au dela de 5, le nom genere devient illisible." } }}Elle est également disponible dans un bloc output, pour refuser d'exposer une
valeur incohérente :
output "nom" { value = random_pet.service.id
precondition { condition = random_pet.service.id != "" error_message = "Le nom genere est vide." }}postcondition : vérifier le résultat
Section intitulée « postcondition : vérifier le résultat »La postcondition s'évalue après l'action, et peut donc inspecter le
résultat réel via self :
resource "random_pet" "service" { length = var.longueur
lifecycle { postcondition { condition = length(self.id) > 0 error_message = "Le provider a rendu une identite vide." } }}self n'est disponible que dans une postcondition : au moment d'une
precondition, la ressource n'existe pas encore.
bloc check : surveiller sans bloquer
Section intitulée « bloc check : surveiller sans bloquer »Le bloc check est le seul des quatre à vivre au premier niveau de la
configuration, et le seul qui n'arrête pas l'apply.
check "sante" { assert { condition = can(regex("-", random_pet.service.id)) error_message = "Le nom genere devrait contenir un separateur." }}Un bloc check peut aussi contenir sa propre data source, ce qui permet
d'interroger le monde réel après déploiement. Attention à un effet de bord :
cette data source est relue à chaque plan, ce qui fait sortir
terraform plan -detailed-exitcode en 2 alors même que le plan n'annonce
aucun changement.
Lire le verdict de toutes vos conditions
Section intitulée « Lire le verdict de toutes vos conditions »terraform show -json expose un tableau checks de premier niveau qui
récapitule les quatre niveaux en une seule lecture. C'est la façon fiable de
vérifier vos conditions, plutôt que de lire la sortie humaine.
terraform show -json | jq '.checks[] | {kind: .address.kind, addr: .address.to_display, status}'{"kind":"var", "addr":"var.taille", "status":"pass"}{"kind":"resource", "addr":"random_pet.p", "status":"pass"}{"kind":"output_value", "addr":"output.nom", "status":"pass"}{"kind":"check", "addr":"check.sante", "status":"pass"}Le champ kind identifie l'origine : var pour une validation, resource
pour une precondition ou une postcondition, output_value pour une
precondition d'output, et check pour un bloc check. Le status vaut
pass, fail ou error.
La documentation signale que cette représentation JSON est expérimentale et peut évoluer, y compris en version mineure. Utilisable pour outiller, à surveiller lors d'une montée de version.
Dépannage
Section intitulée « Dépannage »Les pannes de conditions se répartissent en deux familles, et les confondre fait perdre du temps. Les trois premières lignes du tableau relèvent du système de types : l'expression est syntaxiquement valide, mais elle ne produit pas la valeur attendue, et rien ne le signale. Les suivantes relèvent du moment d'évaluation : la condition est correcte, seul l'instant où Terraform la vérifie explique le comportement observé.
| Symptôme | Cause probable | Solution |
|---|---|---|
| Une branche de ternaire change de type sans prévenir | Conversion automatique vers un type commun | Expliciter avec tostring() ou tonumber() |
var.liste == [] toujours faux | [] est un tuple, pas une list(string) | Comparer la longueur : length(var.liste) == 0 |
a || b && c ne donne pas le résultat attendu | && est prioritaire sur || | Parenthéser explicitement |
terraform validate passe mais le plan échoue | Validation référençant une autre variable | Comportement normal, l'erreur tombe au plan |
Un assert faux ne bloque pas l'apply | C'est la nature du bloc check | Utiliser une precondition pour bloquer |
plan -detailed-exitcode rend 2 sans changement annoncé | Data source dans un bloc check, relue à chaque plan | Comportement attendu, en tenir compte en CI |
self indisponible | Utilisé dans une precondition | self n'existe que dans une postcondition |
À retenir
Section intitulée « À retenir »? :n'est pas un opérateur : la documentation parle d'expression conditionnelle.- Les branches sont converties vers un type commun :
true ? 12 : "hello"rend"12". Explicitez. &&lie plus fort que||: parenthésez dès que les deux se croisent.var.liste == []est toujours faux : comparezlength().- Une condition de
validationpeut lire le disque et référencer une data source. preconditionbloque avant l'action,postconditionaprès, etselfn'existe que dans la seconde.- Un bloc
checkn'arrête jamais l'apply : il avertit. terraform show -jsonexpose un tableauchecksqui donne le verdict des quatre niveaux.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous séparent les deux familles de pièges vues au dépannage : celles qui tiennent au type des valeurs comparées, et celles qui tiennent au moment où Terraform évalue la condition. Chaque réponse donne la sortie réelle qui permet de trancher.
Le tableau de décision
| Mécanisme | Où il vit | Quand | En cas d'échec |
|---|---|---|---|
validation |
bloc variable |
à l'évaluation de la variable | arrête le plan |
precondition |
lifecycle ou output |
avant l'action | arrête avant de créer |
postcondition |
lifecycle |
après l'action | arrête, mais après création |
check |
premier niveau | après l'apply | avertit seulement |
Comment choisir
Utilisezvalidation pour une entrée, precondition pour une hypothèse à garantir avant d'agir, postcondition pour un résultat à contrôler, et check pour une surveillance qui ne doit pas bloquer un déploiement.Vérifié sur Terraform v1.15.4.Le message exact
lifecycle {
precondition {
condition = length(self.id) > 0
}
}
Error: Invalid "self" reference
The "self" object is not available in this context. This object can be used
only in resource provisioner, connection, and postcondition blocks.
La raison
Uneprecondition s'évalue avant l'action : la ressource n'existe pas, ses attributs non plus. Seule la postcondition, évaluée après, peut lire self.Vérifié sur Terraform v1.15.4.Ce qui se passe réellement
Error: Resource postcondition failed
on main.tf line 6, in resource "random_pet" "nom":
6: condition = length(self.id) > 100
La commande échoue. Pourtant :terraform state list
random_pet.nom
La ressource existe. La postcondition s'évalue après l'action, elle ne peut donc pas l'empêcher : elle signale un résultat non conforme sur une ressource déjà créée.La conséquence pratique
Après un échec depostcondition, vous avez un état partiel. Si l'objectif est de ne rien créer du tout en cas de violation, c'est une precondition qu'il faut écrire.Vérifié sur Terraform v1.15.4.Le comportement attendu
Warning: Check block assertion failed
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
L'assertion est fausse, un avertissement est émis, et l'apply réussit. C'est voulu.À quoi sert un check alors
À surveiller une propriété sans risquer de bloquer une livraison : disponibilité d'un point d'entrée, cohérence d'une donnée externe. Le verdict reste consultable :terraform show -json | jq -c '.checks[]'
{"kind":"check","addr":"check.nom_assez_long","status":"fail"}
Pour bloquer
Utilisez uneprecondition, qui arrête avant l'action.Vérifié sur Terraform v1.15.4.La démonstration
echo 'tolist([]) == []' | terraform console
false
Les deux collections sont pourtant vides. Mais [] est un tuple et tolist([]) une liste typée : Terraform compare aussi le type.La forme correcte
echo 'length(tolist([])) == 0' | terraform console
true
Comparez toujours la longueur pour tester la vacuité d'une collection. La même remarque vaut pour les maps avec length(var.map) == 0.Vérifié sur Terraform v1.15.4.La conversion silencieuse
echo 'true ? 1 : "deux"' | terraform console
"1"
La branche retenue rend 1, un nombre, mais le résultat est la chaîne "1". Terraform a converti les deux branches vers un type commun, ici string.La conséquence
Un attribut qui attend un nombre recevra une chaîne, et l'erreur tombera plus loin, sur une ligne sans rapport avec le ternaire.La parade
Gardez les deux branches du même type, ou explicitez la conversion avectostring() ou tonumber().Vérifié sur Terraform v1.15.4.La démonstration
echo 'true || false && false' | terraform console
true
echo '(true || false) && false' | terraform console
false
La règle
&& est prioritaire sur ||. La première expression se lit donc true || (false && false).La bonne pratique
Parenthésez dès que les deux opérateurs se croisent dans une même expression. Le gain de concision ne compense jamais une condition mal lue en revue de code.Vérifié sur Terraform v1.15.4.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Le bloc lifecycle :
create_before_destroy,prevent_destroy, et les conditions portees par la ressource. - Le meta-argument count : le
count = condition ? 1 : 0en action, et ses limites. - for_each : indexer par cle : ce qu'il faut preferer a
countdes que les elements ont une identite. - Conditional expressions : la reference officielle : l'operateur ternaire et la convergence de types.