Aller au contenu
Infrastructure as Code medium

Conditions Terraform : expressions conditionnelles et validation

25 min de lecture

logo terraform

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.

  • 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, bloc check
  • Lire le verdict de toutes vos conditions en une seule commande

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 :

Fenêtre de terminal
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"
}

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)
}

Point systématiquement omis, et source de bugs silencieux. Du plus prioritaire au moins prioritaire :

RangOpérateurs
1! et - unaire
2*, /, %
3+, -
4>, >=, <, <=
5==, !=
6&&
7||

Conséquence directe : && lie plus fort que ||.

Fenêtre de terminal
echo 'true || false && false' | terraform console # true
echo '(true || false) && false' | terraform console # false

La 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.

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 = []
}
ExpressionRésultat
var.vide == []false
var.vide == tolist([])false
length(var.vide) == 0true

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
}

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écanismeOù il vitQuand il s'évalueEffet en cas d'échec
validationbloc variableà l'évaluation de la variablearrête le plan
preconditionlifecycle d'une ressource, ou bloc outputavant l'actionarrête avant de créer
postconditionlifecycle d'une ressourceaprès l'actionarrête après création
bloc checkau premier niveauaprès l'applyavertit seulement
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.

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."
}
}

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.

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.

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.

Fenêtre de terminal
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.

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ômeCause probableSolution
Une branche de ternaire change de type sans prévenirConversion automatique vers un type communExpliciter 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 échoueValidation référençant une autre variableComportement normal, l'erreur tombe au plan
Un assert faux ne bloque pas l'applyC'est la nature du bloc checkUtiliser une precondition pour bloquer
plan -detailed-exitcode rend 2 sans changement annoncéData source dans un bloc check, relue à chaque planComportement attendu, en tenir compte en CI
self indisponibleUtilisé dans une preconditionself n'existe que dans une postcondition
  1. ? : n'est pas un opérateur : la documentation parle d'expression conditionnelle.
  2. Les branches sont converties vers un type commun : true ? 12 : "hello" rend "12". Explicitez.
  3. && lie plus fort que || : parenthésez dès que les deux se croisent.
  4. var.liste == [] est toujours faux : comparez length().
  5. Une condition de validation peut lire le disque et référencer une data source.
  6. precondition bloque avant l'action, postcondition après, et self n'existe que dans la seconde.
  7. Un bloc check n'arrête jamais l'apply : il avertit.
  8. terraform show -json expose un tableau checks qui donne le verdict des quatre niveaux.

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.

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