
Vous voulez une VM avec 2 disques en dev et 5 en prod, ou un groupe de
sécurité AWS avec un nombre variable de règles ingress. Dupliquer
autant de blocs que de cas possibles n’est pas une option : le code
exploserait à chaque nouveau paramètre.
Le bloc dynamic est le mécanisme de Terraform pour générer
un nombre variable de blocs imbriqués à partir d’une variable. Il
s’applique partout où un provider expose des blocs répétables dans
son schéma : ingress et egress dans AWS, env dans Kubernetes,
set dans Helm. Pour les providers dont le schéma expose des
attributs inline (listes d’objets) comme libvirt v0.9+, on utilise
plutôt des expressions for et concat — cette page couvre
les deux approches.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Quand et pourquoi utiliser un bloc
dynamic - Syntaxe
dynamic "nom_bloc" { for_each ... content { ... } } - L’équivalent avec
concat+forpour les providers à attributs inline (libvirt v0.9+) - Lab KVM : VM avec N disques et N interfaces générés depuis des variables
Prérequis
Section intitulée « Prérequis »for_eachcompris (for_each Terraform)- Boucles
formaîtrisées (Boucles for)
L’idée derrière les blocs dynamiques
Section intitulée « L’idée derrière les blocs dynamiques »Pensez à une boucle classique en généralisant des éléments imbriqués :
ingress_rules = [ { "port": 80, "protocol": "tcp" }, { "port": 443, "protocol": "tcp" },]
for rule in ingress_rules: # Générer un bloc ingress {} pour chaque règle create_ingress_block(port=rule["port"], protocol=rule["protocol"])Les blocs dynamic de Terraform font exactement cela : au lieu de copier une structure HCL N fois, vous énumérez les données et Terraform génère le bloc N fois.
Le bloc dynamic
Section intitulée « Le bloc dynamic »Quand l’utiliser
Section intitulée « Quand l’utiliser »Le bloc dynamic s’applique aux ressources qui exposent des blocs imbriqués répétables en tant qu’arguments. C’est courant dans :
- AWS : blocs
ingress {}/egress {}dansaws_security_group - Kubernetes : blocs
env {}dans les conteneurs - Helm : blocs
set {}danshelm_release
⚠️ Note : le provider libvirt v0.9+ n’expose pas de blocs répétables — il utilise des attributs imbriqués (listes d’objets). Pour libvirt, voir la section “Expression for + concat”.
dynamic fonctionne aussi dans les blocs data, provider et
provisioner — pas uniquement dans les resource.
Syntaxe minimale
Section intitulée « Syntaxe minimale »La logique : « pour chaque élément d’une collection, générer un bloc HCL complet avec ses valeurs ».
dynamic "nom_du_bloc" { # ← nom du bloc à générer (ingress, env, set...) for_each = var.ma_collection # ← la collection à itérer content { attr1 = nom_du_bloc.value.field1 # ← accéder à la valeur courante attr2 = nom_du_bloc.value.field2 }}Clé importante : la variable d’itération porte le même nom que le bloc dynamique (ingress.value, env.value, etc.).
Exemple — règles de sécurité AWS
Section intitulée « Exemple — règles de sécurité AWS »variable "ingress_rules" { type = list(object({ port = number protocol = string })) default = [ { port = 80, protocol = "tcp" }, { port = 443, protocol = "tcp" }, { port = 22, protocol = "tcp" }, ]}
resource "aws_security_group" "web" { name = "web-sg"
dynamic "ingress" { # ↑ bloc à générer de manière répétable for_each = var.ingress_rules # ↑ itérer sur chaque règle content { # ↓ attributs du bloc ingress, remplis depuis ingress.value courant from_port = ingress.value.port # ↑ accès à la propriété port to_port = ingress.value.port protocol = ingress.value.protocol # ↑ et à la propriété protocol cidr_blocks = ["0.0.0.0/0"] } }}Résultat : 3 blocs ingress {} créés automatiquement, un par élément de var.ingress_rules.
Filtre conditionnel dans dynamic
Section intitulée « Filtre conditionnel dans dynamic »Ajouter une condition dans le for_each pour exclure certains blocs :
dynamic "ingress" { for_each = [for r in var.ingress_rules : r if r.port != 22] # ↑ filtre : exclure port 22 content { from_port = ingress.value.port to_port = ingress.value.port protocol = ingress.value.protocol }}Le filtre s’applique dans le for_each, pas dans le content. Terraform génère uniquement les blocs correspondant à la condition.
Expression for + concat (libvirt v0.9+)
Section intitulée « Expression for + concat (libvirt v0.9+) »Le provider libvirt v0.9+ (réécriture complète du provider) expose les
structures comme devices sous forme d’attributs imbriqués (disks,
interfaces sont des listes d’objets), pas des blocs répétables
Terraform. La syntaxe dynamic n’y est donc pas applicable.
Le pattern adapté est une expression for dans un local pour
construire la liste attendue.
locals { all_disks = concat( # Disque fixe (élément unique dans une liste) [{ source = { file = { file = libvirt_volume.base.path } } target = { dev = "vda", bus = "virtio" } }],
# Disques variables : générés par for [for idx, k in sort(keys(libvirt_volume.extra)) : { # ↑ boucle for pour chaque disque extra source = { file = { file = libvirt_volume.extra[k].path } } target = { dev = "vd${substr("bcdefgh", idx, 1)}", bus = "virtio" } }] )}
resource "libvirt_domain" "vm" { devices = { disks = local.all_disks }}Exemple : VM avec disques et interfaces variables
Section intitulée « Exemple : VM avec disques et interfaces variables »Objectif
Section intitulée « Objectif »Créer une VM KVM dont le nombre de disques et d’interfaces réseau est
déterminé par des variables. Ni le code de la ressource, ni le main.tf
ne changent — seule la variable évolue.
Fichiers
Section intitulée « Fichiers »variables.tf
variable "extra_disks" { type = list(object({ name = string size_gib = number })) default = [ { name = "data", size_gib = 1 }, { name = "logs", size_gib = 1 }, ]}
variable "extra_networks" { type = list(string) default = ["default"]}main.tf
resource "libvirt_volume" "base" { name = "${var.vm_name}-base.qcow2" pool = "default" target = { format = { type = "qcow2" } } create = { content = { url = var.image_path } }}
resource "libvirt_volume" "extra" { for_each = { for d in var.extra_disks : d.name => d }
name = "${var.vm_name}-${each.key}.qcow2" pool = "default" capacity = each.value.size_gib capacity_unit = "GiB" target = { format = { type = "qcow2" } }
depends_on = [libvirt_volume.base]}
locals { all_disks = concat( [{ source = { file = { file = libvirt_volume.base.path } } target = { dev = "vda", bus = "virtio" } }], [for idx, k in sort(keys(libvirt_volume.extra)) : { source = { file = { file = libvirt_volume.extra[k].path } } target = { dev = "vd${substr("bcdefgh", idx, 1)}", bus = "virtio" } }] )
all_interfaces = concat( [{ model = { type = "virtio" } source = { network = { network = "default" } } }], [for net in var.extra_networks : { model = { type = "virtio" } source = { network = { network = net } } }] )}
resource "libvirt_domain" "vm" { name = var.vm_name type = "kvm" memory = var.memory memory_unit = "MiB" vcpu = var.vcpu
os = { type = "hvm", type_arch = "x86_64", type_machine = "q35" }
devices = { disks = local.all_disks interfaces = local.all_interfaces }
depends_on = [libvirt_volume.base, libvirt_volume.extra]}Résultats réels
Section intitulée « Résultats réels »Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
disk_count = 3extra_disk_names = { "data" = "lab11-vm-data.qcow2" "logs" = "lab11-vm-logs.qcow2"}interface_count = 2vm_name = "lab11-vm"Le plan affichait la liste complète calculée :
devices = { disks = [ { source = { file = { file = "(known after apply)" } } target = { bus = "virtio", dev = "vda" } }, # base { source = { file = { file = "(known after apply)" } } target = { bus = "virtio", dev = "vdb" } }, # data { source = { file = { file = "(known after apply)" } } target = { bus = "virtio", dev = "vdc" } }, # logs ] interfaces = [ { model = { type = "virtio" }, source = { network = { network = "default" } } }, { model = { type = "virtio" }, source = { network = { network = "default" } } }, ]}Tester avec un disque supplémentaire
Section intitulée « Tester avec un disque supplémentaire »terraform apply -var='extra_disks=[ {name="data",size_gib=1}, {name="logs",size_gib=1}, {name="backup",size_gib=2}]' -auto-approveTerraform ajoute uniquement le volume manquant — les deux existants ne sont pas recréés.
Synthèse : deux patterns selon le schéma du provider
Section intitulée « Synthèse : deux patterns selon le schéma du provider »| Schéma du provider | Pattern à utiliser | Exemple de provider |
|---|---|---|
Blocs HCL répétables (block_types) | dynamic "bloc" { for_each ... content {...} } | AWS, GCP, Kubernetes, Helm |
| Attributs imbriqués (liste d’objets) | concat([fixe], [for ...]) dans locals | libvirt v0.9+, azurerm récent |
Ce ne sont pas une ancienne et une nouvelle syntaxe — ce sont deux outils différents qui répondent à deux schémas de provider distincts.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause | Solution |
|---|---|---|
dynamic is not expected here | Le provider utilise des attributs, pas des blocs | Utiliser concat + for dans un local |
Expected a comma dans [elem, for x in ...] | Syntaxe HCL invalide | concat([elem], [for x in ...]) |
| Disques dans le mauvais ordre | Ordre non garanti implicitement | Avec une map, for vers une liste trie déjà par clé ; sort(keys(...)) rend l’ordre explicite |
Device vda en double | Base et extra pointent vers le même index | Vérifier l’offset de substr ("bcdefgh") |
À retenir
Section intitulée « À retenir »dynamic "bloc"génère des blocs répétables — pour les providers dont le schéma expose desblock_types- Avec libvirt v0.9+ (attributs imbriqués) :
concat([fixe], [for ...])danslocals dynamicetforne sont pas interchangeables — chacun répond à un schéma de provider différent- La variable d’itération dans
dynamicporte le nom du bloc :ingress.value,disk.value sort(keys(...))rend l’ordre explicite ;forsur une map trie déjà par clé- Réservez
dynamicaux cas où le nombre de blocs varie réellement — 2-3 blocs fixes restent plus lisibles en dur