Retour au cours

infra / terraform

Expressions et fonctions : for, conditionals

Leçon 81 exercice

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 for expressions
  • Extraire un attribut de toutes les instances d'un count/for_each avec 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.

ExpressionRô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[*].attributSplat : 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

hcl
# --- 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é

  • for expressions transforment listes/maps ; [*] (splat) extrait un attribut de toutes les instances d'un count/for_each.
  • merge, lookup, coalesce, try sont les fonctions les plus utiles pour des configs robustes face aux valeurs absentes.
  • dynamic gé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

1 disponible
1

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

Résoudre l’exercice →