Aller au contenu
Infrastructure as Code medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

Blocs dynamiques Terraform : configuration variable selon le provider

16 min de lecture

logo terraform

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.

  • Quand et pourquoi utiliser un bloc dynamic
  • Syntaxe dynamic "nom_bloc" { for_each ... content { ... } }
  • L’équivalent avec concat + for pour les providers à attributs inline (libvirt v0.9+)
  • Lab KVM : VM avec N disques et N interfaces générés depuis des variables

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 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 {} dans aws_security_group
  • Kubernetes : blocs env {} dans les conteneurs
  • Helm : blocs set {} dans helm_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.

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

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.

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.

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
}
}

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.

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]
}
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
disk_count = 3
extra_disk_names = {
"data" = "lab11-vm-data.qcow2"
"logs" = "lab11-vm-logs.qcow2"
}
interface_count = 2
vm_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" } } },
]
}
Fenêtre de terminal
terraform apply -var='extra_disks=[
{name="data",size_gib=1},
{name="logs",size_gib=1},
{name="backup",size_gib=2}
]' -auto-approve

Terraform 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 providerPattern à utiliserExemple 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 localslibvirt 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.

SymptômeCauseSolution
dynamic is not expected hereLe provider utilise des attributs, pas des blocsUtiliser concat + for dans un local
Expected a comma dans [elem, for x in ...]Syntaxe HCL invalideconcat([elem], [for x in ...])
Disques dans le mauvais ordreOrdre non garanti implicitementAvec une map, for vers une liste trie déjà par clé ; sort(keys(...)) rend l’ordre explicite
Device vda en doubleBase et extra pointent vers le même indexVérifier l’offset de substr ("bcdefgh")
  1. dynamic "bloc" génère des blocs répétables — pour les providers dont le schéma expose des block_types
  2. Avec libvirt v0.9+ (attributs imbriqués) : concat([fixe], [for ...]) dans locals
  3. dynamic et for ne sont pas interchangeables — chacun répond à un schéma de provider différent
  4. La variable d’itération dans dynamic porte le nom du bloc : ingress.value, disk.value
  5. sort(keys(...)) rend l’ordre explicite ; for sur une map trie déjà par clé
  6. Réservez dynamic aux cas où le nombre de blocs varie réellement — 2-3 blocs fixes restent plus lisibles en dur

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn