infra / terraform
Expressions et fonctions : for, conditionals
Explication
Ce que vous allez apprendre
- Écrire une expression conditionnelle (ternaire) pour adapter une valeur selon un environnement
- Transformer des listes et des maps avec des
forexpressions - Extraire un attribut de toutes les instances d'un
count/for_eachavec une splat expression - Utiliser les fonctions HCL les plus utiles :
merge,lookup,coalesce,try - Générer des blocs imbriqués répétés avec
dynamic
Dans quel contexte ?
Un module de groupe de sécurité (leçon précédente) doit gérer trois règles d'entrée différentes (HTTP, HTTPS, SSH), et cette liste de règles est amenée à évoluer régulièrement selon les besoins de chaque équipe qui l'utilise. Écrire un bloc ingress séparé pour chaque règle, à la main, empêche de rendre ce module vraiment paramétrable.
D'abord, la forme la plus simple : la condition ternaire
HCL propose une syntaxe conditionnelle compacte, héritée de nombreux langages de programmation : condition ? valeur_si_vrai : valeur_si_faux. Elle permet d'adapter une seule valeur selon un contexte, comme choisir une instance plus grande en production qu'en développement, sans dupliquer tout le bloc resource.
Une fois cette syntaxe acquise, il reste un besoin plus large : transformer des collections entières
Les for expressions répondent à ce besoin : elles parcourent une liste ou une map existante et en produisent une nouvelle, éventuellement filtrée ou transformée au passage. La syntaxe rappelle volontairement les listes en compréhension de Python, un choix délibéré des concepteurs de HCL pour rester familier aux développeurs venant d'autres langages.
| Expression | Rôle |
|---|---|
[for x in liste : f(x)] | Transforme chaque élément d'une liste |
[for x in liste : f(x) if condition] | Transforme ET filtre en une seule passe |
{for x in liste : k(x) => v(x)} | Transforme une liste en map |
liste[*].attribut | Splat : extrait un attribut de toutes les instances d'un count/for_each |
Prérequis
Cette leçon suppose une bonne maîtrise de count et for_each (leçon sur les resources), ainsi que la syntaxe de base des variables et locals.
Maintenant, un raccourci très pratique quand on a créé plusieurs ressources avec count
Écrire [for w in aws_instance.workers : w.private_ip] fonctionne, mais HCL propose un raccourci plus court pour ce cas précis : la "splat expression" aws_instance.workers[*].private_ip, strictement équivalente mais plus rapide à lire et à écrire dans du code déjà dense.
Il reste un problème récurrent en HCL : gérer les valeurs potentiellement absentes sans faire planter la configuration
Piège fréquent
Accéder directement à un attribut optionnel qui n'existe pas (var.config.option_optionnelle si cette clé n'a pas été définie) fait échouer immédiatement l'évaluation de toute la configuration, sans message d'erreur toujours facile à interpréter. try() capture cet échec et retourne une valeur de secours à la place, rendant la configuration robuste face à des entrées incomplètes.
coalesce() répond à un besoin voisin : retourner la première valeur non-null parmi plusieurs candidates, utile pour définir une valeur par défaut personnalisable.
Le dernier mécanisme à connaître : générer des blocs répétés dynamiquement
Bonne pratique
Dès qu'un module doit accepter un nombre VARIABLE de blocs imbriqués du même type (plusieurs règles ingress dans un groupe de sécurité, par exemple), le bloc dynamic évite d'écrire un bloc statique par cas possible. Il transforme une simple liste de règles, fournie en variable, en autant de blocs ingress générés automatiquement — exactement le problème posé en introduction de cette leçon.
Ces expressions et fonctions maîtrisées, la prochaine leçon aborde un défi organisationnel plus large : comment gérer plusieurs environnements (dev, staging, production) sans dupliquer toute la configuration.
Commandes & code
Expressions et fonctions : for, conditionals
# --- Expressions conditionnelles (ternaire) ---
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.environnement == "production" ? "t3.large" : "t3.micro"
}
# --- for expressions : transforment une collection en une autre ---
variable "noms_utilisateurs" {
default = ["alice", "bob", "carol"]
}
locals {
# transforme une liste en map { "alice" = "ALICE", ... }
utilisateurs_majuscules = { for nom in var.noms_utilisateurs : nom => upper(nom) }
# filtre : ne garde que les noms de plus de 3 caractères
noms_longs = [for nom in var.noms_utilisateurs : nom if length(nom) > 3]
# transforme une liste de maps en liste de strings
noms_tagges = [for nom in var.noms_utilisateurs : "user-${nom}"]
}
# --- Splat expressions : raccourci pour extraire un attribut de toutes les instances d'un for_each/count ---
resource "aws_instance" "workers" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
output "toutes_les_ips" {
value = aws_instance.workers[*].private_ip # équivalent à [for w in aws_instance.workers : w.private_ip]
}
# --- Fonctions les plus utilisées ---
locals {
a = merge({ env = "prod" }, { team = "platform" }) # fusionne deux maps -> {env="prod", team="platform"}
b = concat(["a", "b"], ["c", "d"]) # concatène deux listes -> ["a","b","c","d"]
c = lookup({ dev = "t3.micro", prod = "t3.large" }, var.environnement, "t3.micro") # valeur par défaut si absent
d = coalesce(var.nom_custom, "nom-par-defaut") # premier élément non-null
e = jsonencode({ cle = "valeur" }) # sérialise en JSON (utile pour IAM policies)
f = cidrsubnet("10.0.0.0/16", 8, 2) # calcule un sous-réseau : "10.0.2.0/24"
g = templatefile("${path.module}/user_data.sh.tpl", { hostname = "web-01" }) # rend un template de fichier
h = try(var.config.option_optionnelle, "valeur_defaut") # évite un crash si l'attribut n'existe pas
}
# --- dynamic block : génère plusieurs blocs imbriqués à partir d'une collection ---
variable "regles_ingress" {
default = [
{ port = 80, cidr = "0.0.0.0/0" },
{ port = 443, cidr = "0.0.0.0/0" },
{ port = 22, cidr = "10.0.0.0/8" },
]
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.regles_ingress
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [ingress.value.cidr]
}
}
}Résumé
forexpressions transforment listes/maps ;[*](splat) extrait un attribut de toutes les instances d'uncount/for_each.merge,lookup,coalesce,trysont les fonctions les plus utiles pour des configs robustes face aux valeurs absentes.dynamicgénère des blocs imbriqués répétés (ex : plusieurs règles de sécurité) à partir d'une collection, évitant la duplication.
Exercices pratiques
Mission : rendre un groupe de sécurité vraiment paramétrable
Objectif : Utiliser un bloc dynamic et une fonction de sécurisation pour rendre un module robuste face à des variations de règles ou de configuration.
Contexte
Le security group aws_security_group.web utilise un bloc dynamic "ingress" généré depuis var.regles_ingress, qui contient actuellement 3 règles (HTTP, HTTPS, SSH). Une équipe cliente du module doit maintenant passer à 5 règles (ajout de deux ports applicatifs), sans que personne ne touche au module lui-même. Par ailleurs, un autre module lit var.config.option_optionnelle directement, sans filet de sécurité.