Aller au contenu principal
TerraformInfrastructure as CodeFormation

Locals, outputs et expressions

30 min de lecture Terraform — Chapitre 4

Boucles count et for_each, blocs dynamiques, expressions conditionnelles et fonctions built-in Terraform pour du code DRY.

Tu sais déclarer des variables et des outputs. Maintenant, on passe au niveau supérieur : les boucles, les conditions, les blocs dynamiques et les fonctions built-in. C’est ce qui te permet d’écrire du Terraform DRY — sans répéter la même ressource 15 fois avec des valeurs différentes.

Boucles : count et for_each

count — créer N copies

count crée N exemplaires d’une ressource. Chaque copie est identifiée par son index (count.index).

resource "hcloud_server" "web" {
  count       = var.server_count
  name        = "web-${count.index}"
  server_type = "cx22"
  image       = "ubuntu-24.04"
  location    = "fsn1"
}
# Crée hcloud_server.web[0], hcloud_server.web[1], etc.

Le piège : si tu supprimes un élément au milieu, tous les indices décalent. Terraform détruit et recrée les ressources suivantes. Pour des serveurs stateless, c’est OK. Pour des bases de données, c’est catastrophique.

for_each — itérer par clé

for_each itère sur un map ou un set. Chaque ressource est identifiée par sa clé, pas par un index. Supprimer un élément ne touche pas les autres.

variable "servers" {
  type = map(object({
    type     = string
    location = string
  }))
  default = {
    web = { type = "cx22", location = "fsn1" }
    api = { type = "cx32", location = "nbg1" }
    db  = { type = "cx42", location = "fsn1" }
  }
}

resource "hcloud_server" "app" {
  for_each    = var.servers
  name        = each.key
  server_type = each.value.type
  image       = "ubuntu-24.04"
  location    = each.value.location
}
# Crée hcloud_server.app["web"], hcloud_server.app["api"], etc.

💡 Règle simple : utilise for_each par défaut. Réserve count pour du “N copies identiques” ou du conditionnel (count = condition ? 1 : 0).

L’expression for

En dehors des ressources, for transforme des collections — filtrage, mapping, restructuration :

locals {
  # Transformer une liste
  server_names = [for k, v in var.servers : upper(k)]
  # → ["API", "DB", "WEB"]

  # Filtrer une map
  fsn_servers = {
    for k, v in var.servers : k => v
    if v.location == "fsn1"
  }
  # → { web = {...}, db = {...} }
}

Blocs dynamiques

Quand un bloc imbriqué doit être répété (comme ingress dans un firewall), dynamic évite la duplication :

variable "firewall_rules" {
  type = list(object({
    direction  = string
    port       = string
    protocol   = string
    source_ips = list(string)
  }))
  default = [
    { direction = "in", port = "80",  protocol = "tcp", source_ips = ["0.0.0.0/0"] },
    { direction = "in", port = "443", protocol = "tcp", source_ips = ["0.0.0.0/0"] },
    { direction = "in", port = "22",  protocol = "tcp", source_ips = ["10.0.0.0/8"] },
  ]
}

resource "hcloud_firewall" "web" {
  name = "web-fw"

  dynamic "rule" {
    for_each = var.firewall_rules
    content {
      direction  = rule.value.direction
      port       = rule.value.port
      protocol   = rule.value.protocol
      source_ips = rule.value.source_ips
    }
  }
}

⚠️ N’abuse pas des dynamic blocks. Si la logique devient trop complexe, c’est un signal qu’il faut restructurer en modules. Un dynamic dans un dynamic, c’est toujours un red flag.

Expressions conditionnelles

La syntaxe ternaire de HCL permet des choix simples sans if/else :

locals {
  is_prod = var.environment == "prod"
}

resource "hcloud_server" "web" {
  name        = "web-${var.environment}"
  server_type = local.is_prod ? "cx32" : "cx22"
  image       = "ubuntu-24.04"
  location    = "fsn1"
  backups     = local.is_prod
}

# Ressource conditionnelle : existe seulement en prod
resource "hcloud_floating_ip" "web" {
  count         = local.is_prod ? 1 : 0
  type          = "ipv4"
  home_location = "fsn1"
}

Le pattern count = condition ? 1 : 0 est l’idiome standard pour rendre une ressource optionnelle. Tu le verras dans tous les projets Terraform.

🔥 Astuce : pour référencer une ressource conditionnelle, utilise one(hcloud_floating_ip.web[*].ip_address) — ça retourne null si la ressource n’existe pas au lieu de planter sur un index vide.

Fonctions built-in essentielles

Terraform embarque des dizaines de fonctions. Voici celles que tu utiliseras quotidiennement.

Chaînes et collections

locals {
  # join / split — assembler / découper
  az_csv = join(", ", var.zones)             # "fsn1, nbg1, hel1"
  parts  = split(".", "api.example.com")     # ["api", "example", "com"]

  # format — comme printf
  bucket = format("%s-%s-assets", var.project, var.environment)

  # lookup — accès map avec fallback
  sizes  = { dev = "cx22", staging = "cx22", prod = "cx32" }
  size   = lookup(local.sizes, var.environment, "cx22")

  # merge — fusionner des maps
  tags   = merge(local.common_tags, { Name = "specific" })

  # flatten — aplatir des listes imbriquées
  all_ips = flatten([var.public_ips, var.private_ips])

  # coalesce — première valeur non vide
  name   = coalesce(var.custom_name, "default")
}

Fichiers et templates

# file — lire un fichier
resource "hcloud_ssh_key" "deploy" {
  name       = "deploy"
  public_key = file("~/.ssh/id_ed25519.pub")
}

# templatefile — fichier avec interpolation de variables
resource "hcloud_server" "web" {
  user_data = templatefile("${path.module}/scripts/init.sh.tpl", {
    hostname    = var.server_name
    environment = var.environment
    packages    = join(" ", var.extra_packages)
  })
}

Le template init.sh.tpl utilise ${variable} pour l’interpolation. templatefile remplace l’ancien data "template_file" (déprécié).

💡 Fonctions utiles à connaître aussi : try() pour les valeurs optionnelles, can() pour tester si une expression est valide, cidrsubnet() pour calculer des sous-réseaux, jsonencode()/yamldecode() pour les conversions de format.

Cas entreprise : infrastructure paramétrique

Une équipe SRE gère 12 microservices sur Hetzner. Chaque service a un serveur, un firewall et un volume — mais avec des tailles et des règles différentes.

Sans boucles, c’est 36 blocs resource quasi-identiques. Avec for_each et des locals bien structurés :

locals {
  services = {
    auth    = { size = "cx22", disk = 20,  ports = ["443"] }
    api     = { size = "cx32", disk = 50,  ports = ["443", "8080"] }
    worker  = { size = "cx42", disk = 100, ports = [] }
    # ... 9 autres services
  }
}

Un seul bloc resource avec for_each crée les 12 serveurs. Un dynamic block génère les règles firewall. L’ajout d’un 13e service = une ligne dans la map services.

🎯 Résultat : le code passe de 800 lignes à 120. Les reviews sont plus rapides, les erreurs de copier-coller disparaissent, et l’onboarding d’un nouveau SRE prend une heure au lieu d’une journée.

Les pièges courants

⚠️ count + liste ordonnée — Ne mappe jamais count.index sur une liste qui peut changer d’ordre. Utilise for_each avec un set ou une map à la place.

⚠️ for_each sur une listefor_each accepte un map ou un set, pas une list. Convertis avec toset(var.my_list) ou utilise { for item in var.my_list : item => item }.

⚠️ Computed values dans for_each — Le for_each doit être connu au plan. Si la valeur dépend d’un attribut calculé après apply (comme un ID), Terraform plante. Solution : utilise count ou restructure les dépendances.

⚠️ dynamic blocks illisibles — Un dynamic dans un dynamic avec des conditions, c’est du code que personne ne peut relire. Extrais la logique dans un module ou simplifie la structure de données.

Résumé

Les expressions et boucles transforment Terraform d’un outil de description statique en un vrai langage d’infrastructure :

  • count = N copies identiques ou ressource conditionnelle (? 1 : 0)
  • for_each = itération par clé — toujours préféré à count pour des ressources distinctes
  • dynamic = blocs imbriqués répétés (firewall rules, tags, etc.)
  • Ternaire = condition ? vrai : faux pour les choix simples
  • Fonctions = lookup, merge, format, templatefile, flatten… des dizaines disponibles
  • for = transformer et filtrer des collections dans les locals

Dans le prochain chapitre, on plonge dans le state Terraform — comment il fonctionne en interne, les backends distants, le locking et les opérations avancées.

🖥️ Pratique sur ton propre serveur

Pour suivre Terraform en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.

Obtenir 200$

Articles liés