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 liste — for_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é àcountpour des ressources distinctesdynamic= blocs imbriqués répétés (firewall rules, tags, etc.)- Ternaire =
condition ? vrai : fauxpour les choix simples - Fonctions =
lookup,merge,format,templatefile,flatten… des dizaines disponibles for= transformer et filtrer des collections dans leslocals
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.
Contenu réservé aux abonnés
Ce chapitre fait partie de la formation complète. Abonne-toi pour débloquer tous les contenus.
Débloquer pour 29 CHF/moisLe chapitre 1 de chaque formation est gratuit.
Série pas encore débloquée
Termine la série prérequise d'abord pour accéder à ce contenu.
Aller à la série prérequiseSérie : Terraform
4 / 6Sur cette page
Articles liés
Init, Plan, Apply : le workflow Terraform
Maîtrise le workflow Terraform complet : init, plan, apply, destroy. Puis découvre les resources, data sources, le state et la structure de projet.
Pourquoi l'IaC ? Introduction à Terraform
Découvre l'Infrastructure as Code, installe Terraform, apprends la syntaxe HCL, les providers et réalise ton premier déploiement sur Hetzner.
Variables et types de données
Maîtrise les variables d'entrée Terraform, les différents types (string, number, list, map, object), l'assignation de valeurs et les outputs.