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

Séparer dev, staging et prod avec Terraform

16 min de lecture

logo terraform

Pour séparer dev, staging et prod avec Terraform, créez un module commun contenant le code d’infrastructure, puis un répertoire par environnement qui appelle ce module avec ses propres paramètres. Chaque environnement possède son propre state si vous gardez le backend local par répertoire ou si vous configurez un backend distant distinct par environnement. C’est la méthode recommandée par HashiCorp pour les projets professionnels.

Prérequis : avoir lu Les workspaces Terraform et Quand utiliser les workspaces pour comprendre pourquoi cette approche remplace les workspaces en contexte professionnel.

  • Créer un module Terraform réutilisable
  • Configurer un répertoire par environnement avec ses propres paramètres
  • Vérifier l’isolation des states selon le backend utilisé
  • Prouver qu’un destroy dans un environnement ne touche pas les autres

Voici l’arborescence que vous allez mettre en place :

mon-projet/
├── modules/
│ └── reseau/
│ ├── versions.tf # Contraintes de providers
│ ├── variables.tf # Paramètres du module
│ ├── main.tf # Ressources communes
│ └── outputs.tf # Valeurs exposées
├── envs/
│ ├── dev/
│ │ ├── backend.tf # Backend local ou distant spécifique à dev
│ │ ├── main.tf # Appelle le module avec les paramètres dev
│ │ └── outputs.tf # Expose les valeurs dev
│ └── prod/
│ ├── backend.tf # Backend local ou distant spécifique à prod
│ ├── main.tf # Appelle le module avec les paramètres prod
│ └── outputs.tf # Expose les valeurs prod

Chaque répertoire dans envs/ est un projet Terraform indépendant avec son propre terraform init et son propre cycle de vie. Avec le backend local par défaut, chaque répertoire garde naturellement son terraform.tfstate. Avec un backend distant, l’isolation dépend de la configuration du backend : bucket, key, workspace backend ou credentials doivent être distincts selon le niveau d’isolation recherché.

En lab local, vous pouvez ne rien déclarer : chaque répertoire gardera son propre terraform.tfstate local. En contexte professionnel, ajoutez un backend.tf par environnement ou passez des paramètres différents via -backend-config.

envs/dev/backend.tf
terraform {
backend "s3" {
bucket = "terraform-state-dev"
key = "reseau/terraform.tfstate"
region = "eu-west-1"
}
}
envs/prod/backend.tf
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "reseau/terraform.tfstate"
region = "eu-west-1"
}
}

Si vous utilisez le même bucket et la même key pour dev et prod, les deux environnements partageront le même state distant malgré les répertoires séparés.

Le module regroupe tout le code d’infrastructure. Il ne contient aucune valeur en dur — tout passe par des variables.

  1. Créer le fichier modules/reseau/versions.tf

    Ce fichier déclare les contraintes de version du provider, sans configurer le provider lui-même (c’est le rôle de l’appelant) :

    terraform {
    required_version = ">= 1.11.0"
    required_providers {
    libvirt = {
    source = "dmacvicar/libvirt"
    version = "~> 0.8"
    }
    }
    }
  2. Créer le fichier modules/reseau/variables.tf

    Chaque paramètre qui varie entre environnements devient une variable :

    variable "env_name" {
    description = "Nom de l'environnement (dev, staging, prod…)"
    type = string
    }
    variable "network_address" {
    description = "Adresse réseau (ex: 10.10.70.1)"
    type = string
    }
    variable "network_netmask" {
    description = "Masque réseau"
    type = string
    default = "255.255.255.0"
    }
    variable "dhcp_start" {
    description = "Début de la plage DHCP"
    type = string
    }
    variable "dhcp_end" {
    description = "Fin de la plage DHCP"
    type = string
    }
    variable "disk_size_gb" {
    description = "Taille du volume en Go"
    type = number
    }
    variable "base_image_path" {
    description = "Chemin vers l'image de base"
    type = string
    }
  3. Créer le fichier modules/reseau/main.tf

    Les ressources utilisent les variables au lieu de valeurs en dur. Le nom de chaque ressource intègre var.env_name pour éviter les collisions :

    resource "libvirt_network" "env_net" {
    name = "${var.env_name}-network"
    autostart = true
    forward = {
    mode = "nat"
    }
    dns = {
    enabled = true
    }
    ips = [{
    address = var.network_address
    netmask = var.network_netmask
    dhcp = {
    ranges = [{
    start = var.dhcp_start
    end = var.dhcp_end
    }]
    }
    }]
    }
    resource "libvirt_volume" "env_disk" {
    name = "${var.env_name}-disk.qcow2"
    pool = "default"
    backing_store = {
    path = var.base_image_path
    format = {
    type = "qcow2"
    }
    }
    target = {
    format = {
    type = "qcow2"
    }
    }
    capacity = var.disk_size_gb * 1024 * 1024 * 1024
    }
  4. Créer le fichier modules/reseau/outputs.tf

    Les outputs rendent accessibles les identifiants et noms des ressources créées :

    output "network_name" {
    description = "Nom du réseau créé"
    value = libvirt_network.env_net.name
    }
    output "network_id" {
    description = "ID du réseau créé"
    value = libvirt_network.env_net.id
    }
    output "volume_name" {
    description = "Nom du volume créé"
    value = libvirt_volume.env_disk.name
    }
    output "volume_id" {
    description = "ID du volume créé"
    value = libvirt_volume.env_disk.id
    }

Le module est prêt. Il ne fait rien seul — il a besoin d’un appelant qui lui fournit les valeurs.

Chaque environnement appelle le même module avec des paramètres différents.

Créez le fichier envs/dev/main.tf :

terraform {
required_version = ">= 1.11.0"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "infra" {
source = "../../modules/reseau"
env_name = "dev"
network_address = "10.10.70.1"
dhcp_start = "10.10.70.10"
dhcp_end = "10.10.70.100"
disk_size_gb = 4
base_image_path = "/chemin/vers/image.qcow2"
}

Le fichier envs/dev/outputs.tf expose les valeurs du module :

output "network_name" {
value = module.infra.network_name
}
output "volume_name" {
value = module.infra.volume_name
}

Le fichier envs/prod/main.tf appelle le même module mais avec des paramètres production — réseau différent, volume plus gros :

terraform {
required_version = ">= 1.11.0"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "infra" {
source = "../../modules/reseau"
env_name = "prod"
network_address = "10.10.71.1"
dhcp_start = "10.10.71.10"
dhcp_end = "10.10.71.100"
disk_size_gb = 8
base_image_path = "/chemin/vers/image.qcow2"
}
  1. Initialiser et déployer dev

    Fenêtre de terminal
    cd envs/dev
    terraform init

    Terraform télécharge le provider et référence le module local :

    Initializing modules...
    - infra in ../../modules/reseau
    Initializing provider plugins...
    - Installing dmacvicar/libvirt v0.9.7...
    Terraform has been successfully initialized!
    Fenêtre de terminal
    terraform apply -auto-approve
    Plan: 2 to add, 0 to change, 0 to destroy.
    module.infra.libvirt_volume.env_disk: Creation complete after 0s
    module.infra.libvirt_network.env_net: Creation complete after 0s
    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
    Outputs:
    network_name = "dev-network"
    volume_name = "dev-disk.qcow2"
  2. Initialiser et déployer prod

    Sans quitter le répertoire dev, ouvrez un autre terminal ou changez de répertoire :

    Fenêtre de terminal
    cd ../prod
    terraform init
    terraform apply -auto-approve
    Plan: 2 to add, 0 to change, 0 to destroy.
    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
    Outputs:
    network_name = "prod-network"
    volume_name = "prod-disk.qcow2"
  3. Vérifier la coexistence

    Les deux environnements coexistent sur la même machine :

    Fenêtre de terminal
    virsh net-list --all | grep -E 'dev|prod'
    dev-network active yes yes
    prod-network active yes yes
    Fenêtre de terminal
    virsh vol-list default | grep -E 'dev|prod'
    dev-disk.qcow2 /var/lib/libvirt/images/dev-disk.qcow2
    prod-disk.qcow2 /var/lib/libvirt/images/prod-disk.qcow2
  4. Vérifier l’isolation des states

    Chaque environnement a son propre fichier state :

    Fenêtre de terminal
    find . -name "*.tfstate" | sort
    ./envs/dev/terraform.tfstate
    ./envs/prod/terraform.tfstate

    Le state dev ne contient que les ressources dev, le state prod que les ressources prod. Aucun mélange possible.

Prouver l’isolation : détruire dev sans toucher prod

Section intitulée « Prouver l’isolation : détruire dev sans toucher prod »

La force de cette approche : un terraform destroy dans un répertoire ne peut pas affecter les autres.

Fenêtre de terminal
cd envs/dev
terraform destroy -auto-approve
module.infra.libvirt_volume.env_disk: Destroying...
module.infra.libvirt_network.env_net: Destroying...
Destroy complete! Resources: 2 destroyed.

Vérification immédiate — prod est toujours là :

Fenêtre de terminal
virsh net-list --all | grep -E 'dev|prod'
prod-network active yes yes

Un terraform plan dans prod confirme zéro changement :

Fenêtre de terminal
cd ../prod
terraform plan
No changes. Your infrastructure matches the configuration.

Pour ajouter un nouvel environnement, il suffit de créer un répertoire et d’appeler le module :

Fenêtre de terminal
mkdir envs/staging

Créez envs/staging/main.tf :

terraform {
required_version = ">= 1.11.0"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "infra" {
source = "../../modules/reseau"
env_name = "staging"
network_address = "10.10.72.1"
dhcp_start = "10.10.72.10"
dhcp_end = "10.10.72.100"
disk_size_gb = 4
base_image_path = "/chemin/vers/image.qcow2"
}

Puis terraform init && terraform apply. Pas besoin de modifier le module ni les autres environnements.

SymptômeCause probableSolution
Error: Module not foundMauvais chemin relatif dans sourceVérifier que ../../modules/reseau pointe vers le bon dossier
Error: No configuration filesterraform init lancé depuis la racineSe placer dans envs/dev/ ou envs/prod/ avant d’exécuter
Collision de noms de ressourcesMême env_name dans deux environnementsChaque répertoire doit avoir un env_name unique
Deux environnements partagent le même state distantMême backend, bucket ou key réutilisésDéfinir un backend ou au minimum une key différente par environnement
Module modifié mais pas pris en compteCache du module périméRelancer terraform init -upgrade dans l’environnement concerné
Provider version mismatch entre envsLock file divergentCopier le .terraform.lock.hcl à jour ou re-init
  • Un module commun contient le code (ressources, variables, outputs) sans aucune valeur en dur
  • Un répertoire par environnement appelle le module avec les paramètres adaptés (adresses, tailles, noms)
  • Chaque répertoire a son propre cycle de vie ; l’isolation du state est automatique en backend local et doit être configurée explicitement en backend distant
  • Détruire un environnement ne peut pas toucher les autres — l’isolation est structurelle
  • Ajouter un nouvel environnement prend quelques minutes : un répertoire, un main.tf, un terraform init
  • En contexte professionnel, chaque répertoire doit pointer vers un backend adapté avec une key, un bucket ou des credentials qui évitent tout partage involontaire du state

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