
Comment construire le nom lab03-web-0 à partir de variables séparées ?
Comment lire un fichier cloud-init et l'encoder en base64 ? Comment découper
un réseau 10.10.0.0/16 en sous-réseaux ? Ces transformations reviennent dans
chaque projet Terraform.
Terraform embarque 238 fonctions intégrées en version 1.15 (le décompte
exact de votre version s'obtient avec terraform metadata functions -json),
directement dans HCL, sans import ni module externe. Elles ne modifient pas
l'infrastructure, elles transforment des valeurs : construire un nom avec
format(), découper un réseau avec cidrsubnet(), fusionner des maps
avec merge(), lire un fichier avec file(). Elles s'utilisent dans les
locals, les outputs et les attributs de ressources.
Tous les exemples de ce guide ont été exécutés sur Terraform v1.15.4 avant
publication, résultats compris. C'est nécessaire : plusieurs fonctions se
comportent autrement que l'intuition ne le suggère. element() hors bornes,
lookup() sans valeur par défaut et l'échappement de templatefile() sont
trois pièges qui n'apparaissent qu'à l'exécution.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »terraform console: tester une fonction en une seconde, sans projet- Par type de donnée : chaînes, nombres, listes, maps, fichiers
- Réseau IP :
cidrsubnet,cidrhost,cidrnetmask - Robustesse :
try,can,coalesce,one - Données sensibles :
sensitive,nonsensitive - Fonctions définies par les providers : la syntaxe
provider:: - Quand une fonction est évaluée : au plan ou à l'apply, et pourquoi ça compte
Prérequis
Section intitulée « Prérequis »- Terraform installé (installer Terraform).
Les fonctions de ce guide existent depuis longtemps ; seules les fonctions de
provider exigent Terraform 1.8 ou plus récent, et
templatestring1.9 ou plus récent. - Variables et locals maîtrisés (variables, locals).
terraform console : le bac à sable des fonctions
Section intitulée « terraform console : le bac à sable des fonctions »Sans la console, vérifier une expression coûte un aller-retour complet :
écrire la ligne dans un fichier, lancer terraform plan, attendre l'évaluation
de toute la configuration, puis lire un message qui désigne la ligne fautive
sans jamais montrer la valeur produite. Sur une expression imbriquée, on
répète l'opération plusieurs fois avant de comprendre où la donnée dérape.
terraform console supprime cet aller-retour. C'est un interpréteur
interactif : vous tapez une expression, il affiche la valeur obtenue. Le
gain n'est pas seulement la vitesse, c'est de voir la donnée au lieu de la
deviner à partir d'un message d'erreur.
Trois usages reviennent en permanence :
- Vérifier ce que rend une fonction avant de l'écrire, en particulier sur les fonctions au comportement contre-intuitif présentées dans ce guide.
- Lever un doute sur un type quand un provider refuse une valeur. La console
ne se contente pas d'afficher le contenu, elle affiche le type dans sa
notation :
tolist([...])pour une liste typée,toset([...])pour un ensemble, des crochets nus pour un tuple, des accolades pour un objet. - Reproduire un cas limite (liste vide, clé absente, index hors bornes) pour observer l'erreur exacte, sans toucher à l'infrastructure.
Ce dernier point mérite d'être souligné : la console évalue des expressions, elle ne crée rien. Aucune ressource n'est créée, modifiée ni détruite, ce qui autorise à y tester n'importe quel cas tordu sans conséquence.
terraform console fonctionne dans un répertoire vide, sans aucun
terraform init. C'est ce qui en fait un réflexe praticable : nul besoin de
préparer un projet pour tester une idée.
echo 'upper("lab06-vm")' | terraform console"LAB06-VM"La différence de notation entre un tuple et une liste typée se constate directement :
echo 'tolist(["x","y"])' | terraform consoletolist([ "x", "y",])En mode interactif :
terraform console> format("%s-%s.qcow2", "dev", "lab06-vm")"dev-lab06-vm.qcow2"> min(256, 512, 1024)256> exitL'initialisation n'est requise que si l'expression référence un provider ou un
module : une fonction de provider (provider::…), une data source, une
ressource. Pour les fonctions intégrées, un répertoire vide suffit.
Pour savoir si une fonction existe dans votre version, interrogez la liste des signatures plutôt que la documentation :
terraform metadata functions -json | jq -r '.function_signatures | keys[]' | wc -l238Attention à la portée de cette commande : elle ne retourne que les fonctions
du langage. Les fonctions apportées par un provider n'y figurent pas, il
faut alors lire terraform providers schema -json. Ne concluez donc pas qu'une
fonction de provider n'existe pas au motif qu'elle est absente de cette liste.
Fonctions de chaînes
Section intitulée « Fonctions de chaînes »Les fonctions de chaînes servent presque toujours au même besoin : produire un identifiant conforme à ce qu'attend le provider. Toutes retournent une nouvelle chaîne, aucune ne modifie la valeur d'origine.
format, construire une chaîne avec des placeholders
Section intitulée « format, construire une chaîne avec des placeholders »format() applique un gabarit dans lequel chaque marqueur % est remplacé
par un argument, pris dans l'ordre. Il est plus lisible qu'une interpolation
multi-parties dès trois morceaux, et il sait imposer un remplissage que
${...} ne produit pas.
locals { disk_name = format("%s-%s.qcow2", var.environment, var.vm_name) # "dev-lab06-vm.qcow2"
# Remplissage a zero : indispensable pour un tri correct plus loin index_padded = format("%03d", 7) # "007"}Placeholders courants : %s (string), %d (entier), %f (flottant),
%v (valeur quelconque). La forme %03d complète à gauche avec des zéros.
upper / lower, changer la casse
Section intitulée « upper / lower, changer la casse »Plusieurs ressources cloud refusent les majuscules dans un nom, alors qu'une
variable saisie par un humain en contient souvent. Appliquer lower() à
l'entrée évite un échec au moment de l'apply.
locals { vm_name_lower = lower(var.vm_name) # "lab06-vm"}replace, substitution dans une chaîne
Section intitulée « replace, substitution dans une chaîne »replace(chaîne, motif, remplacement) échange toutes les occurrences. Si le
motif est encadré par des barres obliques ("/^web-/"), Terraform l'interprète
comme une expression régulière ; sinon la recherche est littérale.
locals { hostname_replaced = replace(var.vm_name, "-", "_") # "lab06_vm" sans_prefixe = replace("web-01", "/^web-/", "") # "01"}trimspace, supprimer les espaces en début et fin
Section intitulée « trimspace, supprimer les espaces en début et fin »Le cas d'usage réel n'est pas le littéral mais la sortie d'un fichier lu avec
file(), qui se termine presque toujours par un saut de ligne invisible et casse
la comparaison attendue.
locals { cle_ssh = trimspace(file("${path.module}/id_ed25519.pub"))}split et join, passer de la chaîne à la liste
Section intitulée « split et join, passer de la chaîne à la liste »split(séparateur, chaîne) produit une liste, join() fait le trajet
inverse. C'est la porte d'entrée habituelle quand une valeur arrive en CSV,
par exemple depuis une variable d'environnement de CI qui ne transporte que du
texte.
echo 'split(",", "dev,staging,prod")' | terraform consoletolist([ "dev", "staging", "prod",])locals { joined_envs = join(", ", ["dev", "staging", "prod"]) # "dev, staging, prod"}Fonctions numériques
Section intitulée « Fonctions numériques »Terraform manipule un seul type number, qui accepte les décimales : une
division rend donc un flottant, alors que la plupart des attributs de
dimensionnement exigent un entier.
ceil et floor, arrondir
Section intitulée « ceil et floor, arrondir »Le choix n'est pas cosmétique : pour une capacité de disque ou de
mémoire, arrondir vers le bas produit une ressource trop petite, alors que
ceil() garantit qu'elle reste suffisante.
locals { memory_gib = ceil(512 / 1024) # ceil(0.5) donne 1 disk_gb = floor(3.758) # donne 3}min et max, borner une valeur
Section intitulée « min et max, borner une valeur »Combinées, elles contraignent une valeur dans un intervalle :
min(max(var.memory, 512), 8192) refuse de descendre sous 512 Mo et de dépasser
8 Go. Pour les appliquer à une liste, il faut la développer avec ... :
echo 'min([256, 512, 1024]...)' | terraform console256Fonctions de listes
Section intitulée « Fonctions de listes »Les fonctions de listes préparent les données qui alimentent count et
for_each : count réclame un entier, for_each réclame un set ou une
map, et une liste brute ne convient telle quelle ni à l'un ni à l'autre.
length, taille d'une collection
Section intitulée « length, taille d'une collection »length() retourne le nombre d'éléments d'une liste, d'un set ou d'une map, et
le nombre de caractères d'une chaîne.
locals { environments = ["dev", "staging", "prod"] env_count = length(local.environments) # 3}element, accéder par index avec rebouclage
Section intitulée « element, accéder par index avec rebouclage »element(liste, index) reboucle modulo la longueur de la liste quand l'index
dépasse. C'est le piège le plus répandu de cette fonction : elle ne retombe
pas sur le premier élément, elle calcule index % length.
echo 'element(["dev","staging","prod"], 5)' | terraform console"prod"L'index 5 sur trois éléments donne 5 % 3 = 2, donc "prod". L'index 3 donnerait
bien "dev", mais uniquement parce que 3 est un multiple exact de la longueur.
Deux comportements à connaître, vérifiés en 1.15.4 :
| Appel | Résultat |
|---|---|
element(["dev","staging","prod"], 5) | "prod" (rebouclage modulo) |
element(["dev","staging","prod"], -1) | "prod" (dernier élément) |
element([], 0) | Erreur : la fonction refuse une liste vide |
Le cas de la liste vide n'est pas théorique : il survient dès qu'un split() ne
trouve rien. Pour un accès strict qui échoue sur un index hors bornes,
utilisez la syntaxe liste[index].
sort, trier une liste de chaînes
Section intitulée « sort, trier une liste de chaînes »sort() trie lexicographiquement, jamais numériquement. Sur des nombres, le
résultat surprend :
echo 'sort([3, 10, 2])' | terraform consoletolist([ "10", "2", "3",])"10" passe avant "2" parce que la comparaison se fait caractère par
caractère. Pour trier des nombres, il faut les remplir à gauche avant de
trier, puis reconvertir :
echo '[for s in sort([for x in [3,10,2] : format("%09d", x)]) : tonumber(s)]' | terraform console[ 2, 3, 10,]Une précision qui évite un faux remède : convertir simplement les nombres en
chaînes, avec sort([for x in liste : tostring(x)]), reproduit exactement le
tri lexicographique fautif. Le résultat reste ["10", "2", "3"]. Seul le
remplissage à largeur fixe par format("%09d", x) donne un ordre numérique
correct.
contains, tester l'appartenance
Section intitulée « contains, tester l'appartenance »contains(liste, valeur) retourne un booléen. C'est le socle des blocs
validation : il refuse une valeur hors de la liste autorisée dès le plan,
plutôt que de laisser le provider échouer au milieu de l'apply.
variable "environnement" { type = string validation { condition = contains(["dev", "staging", "prod"], var.environnement) error_message = "L'environnement doit valoir dev, staging ou prod." }}toset, dédupliquer et préparer for_each
Section intitulée « toset, dédupliquer et préparer for_each »toset() convertit une liste en set : ni doublon, ni position. C'est ce qui
permet à for_each d'utiliser la valeur de chaque élément comme clé
d'instance dans le state, à la place d'un index numérique fragile.
echo 'toset(["b","a","a","c"])' | terraform consoletoset([ "a", "b", "c",])flatten, one et zipmap
Section intitulée « flatten, one et zipmap »Ces trois fonctions deviennent incontournables dès que l'on combine for_each
et modules.
echo 'flatten([["a","b"],["c"]])' | terraform console # ["a","b","c"]echo 'zipmap(["a","b"],[1,2])' | terraform console # {"a"=1, "b"=2}echo 'one(["seul"])' | terraform console # "seul"echo 'one([])' | terraform console # nullflatten() aplatit les listes imbriquées produites par une double boucle for.
zipmap() construit une map à partir de deux listes parallèles. one() extrait
l'élément unique d'une liste, ou rend null si elle est vide : c'est la façon
propre de lire une ressource créée avec count = var.actif ? 1 : 0.
Fonctions de maps
Section intitulée « Fonctions de maps »Une map associe des clés à des valeurs. C'est la structure de référence pour les tags et pour les tables de correspondance entre un environnement et un dimensionnement.
keys et values, extraire les clés ou les valeurs
Section intitulée « keys et values, extraire les clés ou les valeurs »Les clés sont retournées triées par ordre lexicographique, ce qui garantit que les deux listes gardent le même ordre d'une exécution à l'autre.
variable "tags" { type = map(string) default = { projet = "demo", equipe = "devops", auteur = "terraform" }}
locals { tag_keys = keys(var.tags) # ["auteur", "equipe", "projet"] tag_values = values(var.tags) # ["terraform", "devops", "demo"]}merge, fusionner des maps
Section intitulée « merge, fusionner des maps »merge() combine plusieurs maps. En cas de clé dupliquée, la dernière map
gagne. C'est ce qui autorise une surcharge ciblée sans réécrire le socle de
tags communs.
locals { tags_merged = merge(var.tags, { env = var.environment }) # {auteur="terraform", env="dev", equipe="devops", projet="demo"}}lookup, lire une clé avec une valeur de repli
Section intitulée « lookup, lire une clé avec une valeur de repli »lookup() sans troisième argument lève une erreur si la clé est absente, il ne
retourne jamais null. La forme à deux arguments est de surcroît dépréciée
depuis Terraform 0.7, parce qu'elle équivaut alors à la syntaxe native
map[clé].
echo 'lookup({a = "alpha"}, "zz")' | terraform consoleError: Invalid function argument the given object has no attribute "zz"Passez donc toujours la valeur de repli :
locals { instance_size = lookup({ dev = "small" staging = "medium" prod = "large" }, var.environment, "small")}Robustesse : try et can
Section intitulée « Robustesse : try et can »Ces deux fonctions traitent le cas d'une valeur dont la forme n'est pas
garantie : un attribut optionnel, une structure venant d'un jsondecode(), une
entrée utilisateur.
can(expression)rend un booléen : l'expression a-t-elle pu être évaluée ?try(expr1, expr2, …)rend la première expression qui s'évalue sans erreur.
echo 'can(tonumber("abc"))' | terraform console # falseecho 'try(tonumber("abc"), 0)' | terraform console # 0echo 'try({a=1}.b, "absent")' | terraform console # "absent"can() sert principalement dans un bloc validation, où il transforme une
erreur d'évaluation en simple false :
variable "port" { type = string validation { condition = can(tonumber(var.port)) error_message = "Le port doit être un nombre." }}La documentation officielle assortit try() d'un avertissement qu'il faut
prendre au sérieux : en abuser produit une configuration difficile à comprendre
et à maintenir, parce qu'elle masque les erreurs au lieu de les traiter.
Réservez-la aux cas où l'absence de valeur est légitime.
Données sensibles : sensitive et nonsensitive
Section intitulée « Données sensibles : sensitive et nonsensitive »sensitive() marque une valeur comme sensible, ce qui la masque dans les
sorties de la CLI. nonsensitive() retire ce marquage.
echo 'sensitive("secret")' | terraform console # (sensitive value)echo 'nonsensitive(sensitive("secret"))' | terraform console # "secret"Le marquage se propage : toute expression dérivée d'une valeur sensible
devient sensible à son tour. C'est utile, mais cela peut casser une
configuration qui fonctionnait, notamment parce qu'une valeur sensible est
interdite en argument de for_each. nonsensitive() existe pour ces cas,
et doit rester exceptionnel.
Marquer une valeur ne la retire pas du state, où elle reste en clair. Le sujet est traité en détail dans gérer les données sensibles.
Fonctions de réseau IP
Section intitulée « Fonctions de réseau IP »Ces fonctions calculent des plages d'adresses à partir d'un préfixe, au lieu de les écrire en dur. C'est ce qui permet de découper un réseau une seule fois et de dériver tout le reste.
echo 'cidrsubnet("10.10.0.0/16", 8, 3)' | terraform console # "10.10.3.0/24"echo 'cidrhost("10.10.3.0/24", 10)' | terraform console # "10.10.3.10"echo 'cidrnetmask("10.10.3.0/24")' | terraform console # "255.255.255.0"cidrsubnet(préfixe, bits_ajoutés, numéro)découpe un réseau en sous-réseaux. Ici on ajoute 8 bits au/16pour obtenir des/24, et on prend le quatrième (numéro 3, en comptant depuis zéro).cidrhost(préfixe, numéro)calcule l'adresse d'un hôte dans un réseau : parfait pour fixer l'adresse d'une passerelle ou d'un serveur.cidrnetmask(préfixe)convertit un préfixe en masque décimal, format que réclament encore certaines configurations réseau.
Fonctions de date et de hachage
Section intitulée « Fonctions de date et de hachage »echo 'formatdate("YYYY-MM-DD", "2026-07-24T10:00:00Z")' | terraform console # "2026-07-24"echo 'timeadd("2026-07-24T10:00:00Z", "24h")' | terraform console # "2026-07-25T10:00:00Z"echo 'sha256("terraform")' | terraform console # "94dc3ea5…"echo 'base64sha256("terraform")' | terraform console # "lNw+pXch…"filesha256() calcule l'empreinte d'un fichier sans le charger en mémoire :
c'est la façon habituelle de déclencher le remplacement d'une ressource quand un
script change.
Fonctions de fichiers
Section intitulée « Fonctions de fichiers »Ces fonctions font entrer dans la configuration du contenu qui n'a rien à faire dans du HCL : une clé SSH, un script cloud-init, un certificat.
file, lire un fichier
Section intitulée « file, lire un fichier »Un chemin relatif nu est résolu depuis le répertoire d'exécution de Terraform,
pas depuis le répertoire du module. C'est la raison pour laquelle il faut
préfixer par path.module dans tout module destiné à être appelé ailleurs.
Vérification : un module contenant file("data.txt") lit le data.txt de la
racine appelante, alors que file("${path.module}/data.txt") lit bien le
sien.
locals { # Correct dans un module : le fichier suit le module cle_ssh = trimspace(file("${path.module}/id_ed25519.pub"))}Le fichier doit exister au moment du plan et être versionné avec le code : un
fichier produit par l'apply d'une autre ressource ne peut pas être lu ainsi.
file() suppose de l'UTF-8 valide et rejette un contenu binaire, pour lequel
il faut filebase64().
templatefile, un fichier avec interpolation
Section intitulée « templatefile, un fichier avec interpolation »templatefile() lit un fichier puis remplace les marqueurs ${...} par les
valeurs de la map passée en second argument. Le template n'a accès qu'à ces
variables, jamais aux var.* ou local.* du module.
locals { user_data = templatefile("${path.module}/cloud-init.yml.tpl", { hostname = local.hostname ssh_key = trimspace(file("${path.module}/id_ed25519.pub")) })}Un template correct s'écrit donc ainsi :
#!/usr/bin/env bashecho "home=$HOME" # $ litteral : rien a echapperecho "date=$(date)" # substitution shell : rien a echapperecho "hote=${hostname}" # interpolation Terraformecho "litteral=$${AUTRE}" # rend le texte ${AUTRE}, non interprete par Terraformtemplatestring, pour un template obtenu dynamiquement
Section intitulée « templatestring, pour un template obtenu dynamiquement »templatestring() (Terraform 1.9 et plus) rend un template qui arrive d'une
autre source : une variable, un fichier lu ailleurs, un attribut de ressource.
Elle refuse un template écrit en littéral. Terraform répond alors : « templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template ». Pour un littéral, utilisez une expression de template classique.
variable "gabarit" { type = string default = "Bonjour $${nom}, tu as $${age} ans"}
output "message" { value = templatestring(var.gabarit, { nom = "alice", age = 30 }) # "Bonjour alice, tu as 30 ans"}base64encode, encoder pour un provider
Section intitulée « base64encode, encoder pour un provider »Certains providers transportent le contenu dans un champ qui n'accepte pas les sauts de ligne.
locals { user_data_b64 = base64encode(local.user_data)}Fonctions définies par les providers
Section intitulée « Fonctions définies par les providers »On ne peut pas définir ses propres fonctions en HCL, mais un provider peut en
exposer. Depuis Terraform 1.8, elles s'appellent avec la syntaxe
provider::<nom_local>::<fonction>.
Le point qui piège : <nom_local> est le nom déclaré dans
required_providers, pas le nom du provider. Si vous le renommez, le préfixe
change avec lui.
Le provider intégré terraform permet de tester tout cela sans le moindre
téléchargement :
terraform { required_version = ">= 1.8" required_providers { tfcore = { source = "terraform.io/builtin/terraform" } }}
output "expr" { value = provider::tfcore::encode_expr([1, "deux"]) }output "tfvars" { value = provider::tfcore::encode_tfvars({ region = "eu-west-1", n = 3 }) }output "decode" { value = provider::tfcore::decode_tfvars("n = 3\nregion = \"eu-west-1\"\n") }Résultat après terraform apply :
decode = { "n" = 3, "region" = "eu-west-1" }expr = "[1, \"deux\"]"tfvars = "n = 3\nregion = \"eu-west-1\"\n"decode_tfvars préserve les types : n ressort en nombre, pas en chaîne.
Avec le nom local tfcore, écrire provider::terraform::encode_expr(1) échoue
sur Unknown provider, ce qui confirme que le préfixe suit bien la déclaration.
Quand une fonction est-elle évaluée ?
Section intitulée « Quand une fonction est-elle évaluée ? »Toutes les erreurs de fonctions ne tombent pas au plan. Une fonction appliquée à une valeur encore inconnue au moment du plan, parce qu'elle provient d'une ressource non encore créée, n'est évaluée qu'à l'apply. L'erreur survient alors après que des ressources ont été créées.
La démonstration tient en quelques lignes :
resource "random_pet" "x" { length = 2 }
# random_pet.x.id est inconnu au plan : la fonction n'est pas evaluee maintenantoutput "casse" { value = tonumber(random_pet.x.id) }Le terraform plan réussit et rend le code 0. Le terraform apply échoue sur
Invalid function argument, et terraform state list montre que
random_pet.x a bien été créé. Corriger l'expression ne suffit donc pas
toujours : il faut ensuite réconcilier l'état partiel.
C'est la raison pour laquelle une validation explicite, par un bloc validation
ou une precondition, vaut mieux qu'une fonction qui échouera peut-être trop
tard.
Référence rapide
Section intitulée « Référence rapide »Le bon angle de recherche est le type de donnée manipulé : quand une expression échoue, c'est presque toujours parce que la fonction n'accepte pas le type reçu.
| Catégorie | Fonctions clés |
|---|---|
| Chaînes | format, upper, lower, replace, trimspace, split, join, substr, startswith, endswith |
| Nombres | ceil, floor, min, max, abs, pow, signum, parseint |
| Listes | length, element, sort, contains, toset, flatten, concat, distinct, range, one |
| Maps | keys, values, merge, lookup, zipmap, tomap |
| Fichiers | file, templatefile, templatestring, base64encode, filebase64, fileexists |
| Réseau IP | cidrsubnet, cidrhost, cidrnetmask, cidrsubnets |
| Date et heure | timestamp, timeadd, timecmp, formatdate |
| Hachage | sha256, filesha256, base64sha256, bcrypt, uuid |
| Robustesse | try, can, coalesce, coalescelist, nonsensitive, sensitive |
| Encodage et types | jsonencode, jsondecode, yamlencode, tostring, tonumber, tobool |
La référence complète est dans la documentation officielle des fonctions Terraform.
Dépannage
Section intitulée « Dépannage »Les erreurs de fonctions se lisent mal, parce que le message désigne la
fonction appelée et non la donnée qui pose problème. Le tableau ci-dessous
part donc du symptôme observé pour remonter à la cause. Les deux premières
lignes concernent le même message d'erreur, Invalid function argument : ce qui
les distingue est le moment où il apparaît, au plan ou à l'apply, et c'est
ce moment qui désigne le correctif.
| Symptôme | Cause probable | Solution |
|---|---|---|
Invalid function argument sur lookup | Clé absente et aucune valeur de repli | Passer un troisième argument : lookup(map, clé, défaut) |
Invalid function argument au moment de l'apply | Fonction appliquée à une valeur inconnue au plan | Valider en amont avec can() ou une precondition |
Call to unknown function | Faute de frappe, ou fonction de provider non déclarée | Vérifier terraform metadata functions -json, et required_providers pour un préfixe provider:: |
Unknown provider sur provider::x::f | Le nom local ne correspond pas à required_providers | Le préfixe suit le nom local, pas le nom du provider |
Script cloud-init cassé, $$ visible | Tous les $ ont été échappés | N'échapper que ${ en $${ |
| Tri de nombres incorrect | sort() trie lexicographiquement | Remplir à largeur fixe avec format("%09d", x) avant de trier |
| Un plan annonce un changement à chaque exécution | Fonction impure (timestamp, uuid, bcrypt) | Sortir la valeur du state ou utiliser ignore_changes |
element() rend une valeur inattendue | Rebouclage modulo sur index hors bornes | Utiliser liste[index] pour un accès strict |
À retenir
Section intitulée « À retenir »terraform consolefonctionne sansinitdans un répertoire vide : testez toujours avant d'écrire.element()reboucle modulo, elle ne retombe pas sur le premier élément, et refuse une liste vide.lookup()sans valeur de repli lève une erreur, et sa forme à deux arguments est dépréciée.sort()trie lexicographiquement :format("%09d", x)avant de trier pour des nombres.- Dans
templatefile, n'échappez que${: tout échapper casse le script. - Un chemin nu dans
file()part du répertoire d'exécution, d'oùpath.moduledans un module. - Une fonction sur une valeur inconnue au plan n'échoue qu'à l'apply, après création partielle.
- Les fonctions de provider s'appellent
provider::<nom_local>::<fonction>, le nom local venant derequired_providers.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent presque toutes sur un comportement
contre-intuitif : une fonction qui reboucle au lieu d'échouer, une autre qui
échoue au lieu de rendre null, un tri qui range 10 avant 9. Chaque réponse
donne la commande de vérification correspondante, à rejouer dans
terraform console.
Réponse courte
Non, pas pour les fonctions intégrées.terraform console fonctionne dans un répertoire vide, sans aucun terraform init.cd $(mktemp -d)
echo 'upper("abc")' | terraform console
"ABC"
Quand init devient nécessaire
L'initialisation n'est requise que si l'expression touche quelque chose que Terraform doit charger :| Expression | init requis ? |
|---|---|
upper("abc"), cidrsubnet(...) |
Non |
provider::terraform::encode_expr(1) |
Oui |
| Référence à une data source ou une ressource | Oui |
| Référence à un module | Oui |
Réponse courte
Elle reboucle modulo la longueur de la liste. Elle ne retombe pas sur le premier élément, contrairement à ce qu'affirment de nombreux tutoriels.echo 'element(["dev","staging","prod"], 5)' | terraform console
"prod"
5 % 3 = 2, donc le troisième élément.Les trois comportements à connaître
| Appel | Résultat |
|---|---|
element(["dev","staging","prod"], 5) |
"prod" (rebouclage modulo) |
element(["dev","staging","prod"], -1) |
"prod" (dernier élément) |
element([], 0) |
Erreur : liste vide refusée |
split() ne trouve rien.Pour un accès strict
Utilisez la syntaxe nativeliste[index], qui échoue franchement hors bornes au lieu de reboucler silencieusement.Vérifié sur Terraform v1.15.4.Réponse courte
Non, elle lève une erreur.lookup() ne retourne jamais null sur une clé absente.echo 'lookup({dev = "small"}, "qa")' | terraform console
Error: Invalid function argument
the given object has no attribute "qa"
La forme à deux arguments est dépréciée
La documentation officielle précise que l'omission du troisième argument est dépréciée depuis Terraform 0.7, parce qu'elle équivaut alors à la syntaxe nativemap[clé].La bonne écriture
locals {
taille = lookup({
dev = "small"
staging = "medium"
prod = "large"
}, var.environment, "small")
}
Vérifié sur Terraform v1.15.4.Le problème
sort() trie lexicographiquement, caractère par caractère.echo 'sort([3, 10, 2])' | terraform console
tolist([
"10",
"2",
"3",
])
"10" passe avant "2" parce que le caractère 1 précède le 2.Le faux remède
Convertir simplement en chaînes avecsort([for x in liste : tostring(x)]) reproduit exactement le même tri fautif. Le résultat reste ["10", "2", "3"].La méthode correcte
Remplir à largeur fixe avant de trier, puis reconvertir :echo '[for s in sort([for x in [3,10,2] : format("%09d", x)]) : tonumber(s)]' | terraform console
[
2,
3,
10,
]
Vérifié sur Terraform v1.15.4.Réponse courte
Non. Seule la séquence${ doit devenir $${ (et %{ devenir %%{). Un $ littéral suivi d'autre chose n'a pas besoin d'être touché.Pourquoi tout échapper casse le script
Échapper systématiquement tous les$ en $$ laisse $$HOME dans le fichier rendu. Or bash interprète $$ comme le PID du processus :bash -c 'echo "home=$$HOME"'
home=786103HOME
La variable est détruite. Même effet sur une substitution de commande : $$(date) devient 786103(date).Le template correct
#!/usr/bin/env bash
echo "home=$HOME" # $ litteral : rien a echapper
echo "date=$(date)" # substitution shell : rien a echapper
echo "hote=${hostname}" # interpolation Terraform
echo "litteral=$${AUTRE}" # rend le texte ${AUTRE}
Vérifié sur Terraform v1.15.4.Réponse courte
Pas en HCL. La documentation officielle le formule ainsi : on ne peut pas définir ses propres fonctions dans le langage de configuration, mais on peut développer un provider qui en expose.La syntaxe
provider::<nom_local>::<fonction>(...)
Disponible depuis Terraform 1.8.Le piège du nom local
<nom_local> est le nom déclaré dans required_providers, pas le nom du provider. Si vous le renommez, le préfixe change avec lui.Tester sans rien télécharger
Le provider intégréterraform expose trois fonctions, utilisables hors ligne :terraform {
required_version = ">= 1.8"
required_providers {
tfcore = { source = "terraform.io/builtin/terraform" }
}
}
output "expr" { value = provider::tfcore::encode_expr([1, "deux"]) }
Résultat : "[1, \"deux\"]".Avec le nom local tfcore, écrire provider::terraform::encode_expr(1) échoue sur Unknown provider.Vérifié sur Terraform v1.15.4.Réponse courte
Parce que la fonction porte sur une valeur inconnue au moment du plan. Terraform ne peut pas l'évaluer avant que la ressource source existe.La démonstration
resource "random_pet" "x" { length = 2 }
# random_pet.x.id est inconnu au plan
output "casse" { value = tonumber(random_pet.x.id) }
Résultat mesuré sur Terraform v1.15.4 :| Étape | Résultat |
|---|---|
terraform plan |
réussit, code retour 0 |
terraform apply |
échoue sur Invalid function argument |
terraform state list |
random_pet.x a été créé |
La conséquence
Corriger l'expression ne suffit pas : il reste un état partiel à réconcilier. C'est la raison pour laquelle une validation explicite, avec un blocvalidation ou une precondition, vaut mieux qu'une fonction qui échouera peut-être trop tard.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Les boucles for de Terraform : transformer une collection, la ou une fonction ne suffit plus.
- Les donnees sensibles : sensitive : la contamination d'une valeur sensible a travers les fonctions.
- Votre premiere infrastructure : mettre ces fonctions au service d'une vraie ressource.
- Built-in functions : la reference officielle : la bibliotheque complete, fonction par fonction.
- try : la fonction qui rattrape une expression invalide.