Aller au contenu principal

Terraform : multi-environnements

· 7 minutes de lecture

Une infrastructure ne reste jamais réduite à un seul environnement. Dev, staging et prod partagent la même architecture (réseau, base de données, application) mais diffèrent sur des paramètres précis : taille des instances, plage CIDR, nombre de réplicas, niveau de logging. Terraform propose deux approches pour gérer cette variation sans dupliquer intégralement la configuration : les workspaces natifs et la structuration par répertoires.

Le problème

Une configuration Terraform unique produit une infrastructure unique par état (state). Appliquer la même configuration pour dev et prod avec un seul state écraserait l'un avec l'autre : le state ne peut représenter qu'une seule réalité à la fois. Il faut donc autant de states distincts que d'environnements, tout en réutilisant la même définition de ressources.

Deux mécanismes répondent à ce besoin. Les workspaces isolent plusieurs states derrière une seule configuration. La structuration par répertoires duplique l'arborescence des configurations racines, chacune avec son propre state, et fait appel aux mêmes modules pour éviter la duplication du code.

Workspaces Terraform

Un workspace est un state nommé, isolé des autres workspaces, associé à la même configuration .tf. Par défaut, toute configuration a un workspace default.

terraform workspace list
terraform workspace new dev
terraform workspace new prod
terraform workspace select dev

Le workspace actif détermine quel state Terraform lit et écrit lors du prochain plan ou apply. Avec un backend S3, chaque workspace correspond à un chemin distinct sous la même key, géré automatiquement par Terraform (env:/<workspace>/<key>).

La variable terraform.workspace expose le nom du workspace actif à l'intérieur de la configuration. Elle sert à faire varier des valeurs selon l'environnement sans dupliquer les fichiers .tf :

locals {
instance_size = {
dev = "db.t4g.micro"
prod = "db.r6g.large"
}
}

resource "aws_db_instance" "main" {
instance_class = local.instance_size[terraform.workspace]
# ...
}

Limites des workspaces

Les workspaces isolent le state, mais pas la configuration : dev et prod exécutent exactement le même code, avec les mêmes providers, les mêmes versions, la même structure de ressources. Toute différence doit passer par des conditions (terraform.workspace == "prod" ? ... : ...) ou des maps comme ci-dessus, ce qui devient illisible dès que plusieurs paramètres varient.

Le risque le plus concret est l'erreur humaine : un terraform apply lancé sans vérifier le workspace actif applique la configuration au mauvais environnement. terraform.workspace est une simple chaîne de caractères dans un fichier de code ; rien n'empêche structurellement de mélanger les credentials AWS de prod avec un workspace dev sélectionné par erreur.

Les workspaces conviennent à des variations mineures entre environnements proches (une plage CIDR, une taille d'instance) sur un projet de taille modeste. Ils ne conviennent pas quand les environnements doivent diverger sur leur structure (prod avec Multi-AZ, dev sans) ou quand l'isolation doit être garantie au niveau de l'infrastructure d'exécution (comptes AWS séparés, credentials distincts).

Structuration par répertoires

L'alternative consiste à donner à chaque environnement son propre répertoire de configuration racine, avec son propre state, ses propres fichiers de variables, et éventuellement son propre backend. Le code partagé (les ressources elles-mêmes) reste factorisé dans des modules, appelés différemment par chaque environnement.

.
├── modules/
│ ├── network/
│ └── database/
└── envs/
├── dev/
│ ├── main.tf
│ ├── backend.tf
│ └── dev.tfvars
└── prod/
├── main.tf
├── backend.tf
└── prod.tfvars

Chaque répertoire est une configuration Terraform indépendante : terraform init et terraform apply s'y exécutent séparément, avec leur propre state.

Le backend.tf ci-dessous code le bucket et la key en dur pour rester lisible, mais une équipe qui veut éviter de dupliquer ces valeurs peut laisser le backend en configuration partielle (backend "s3" {}) et injecter les paramètres à l'init via -backend-config. C'est l'approche décrite dans l'article sur le state remote S3 ; elle se combine naturellement avec la structuration par répertoires, chaque environnement fournissant son propre fichier .tfbackend.

# envs/prod/backend.tf
terraform {
backend "s3" {
bucket = "mon-projet-tfstate"
key = "prod/terraform.tfstate"
region = "eu-west-3"
encrypt = true
use_lockfile = true
}
}

# envs/prod/main.tf
module "network" {
source = "../../modules/network"
vpc_cidr = "10.1.0.0/16"
project = "mon-projet-prod"
}

module "database" {
source = "../../modules/database"
instance_class = "db.r6g.large"
multi_az = true
}
# envs/dev/main.tf
module "network" {
source = "../../modules/network"
vpc_cidr = "10.0.0.0/16"
project = "mon-projet-dev"
}

module "database" {
source = "../../modules/database"
instance_class = "db.t4g.micro"
multi_az = false
}

prod peut activer Multi-AZ et dev non : la divergence structurelle est possible parce que chaque environnement a son propre main.tf, contrairement aux workspaces où toute la configuration est partagée.

Chaque répertoire ayant son propre bloc provider, il peut aussi cibler un compte AWS, une région ou un rôle distinct. C'est le mécanisme qui garantit l'isolation d'exécution : dev et prod ne partagent pas les mêmes credentials.

# envs/prod/provider.tf
provider "aws" {
region = "eu-west-3"
profile = "mon-projet-prod" # profil AWS CLI dédié à la prod
}
# envs/dev/provider.tf
provider "aws" {
region = "eu-west-3"
profile = "mon-projet-dev" # compte ou profil distinct
}

Avec des workspaces, ce découpage n'est pas possible : le bloc provider est unique et partagé par tous les workspaces. Le ciblage du compte devrait passer par une conditionnelle sur terraform.workspace, ce qui ramène le risque d'erreur évoqué plus haut.

Avantages et limites

Isolation réelle. Deux répertoires distincts peuvent pointer vers deux comptes AWS différents, deux régions différentes, ou deux versions de provider différentes. Aucune variable globale ne relie les deux environnements : se tromper de répertoire (cd envs/dev) est une erreur bien plus visible que sélectionner le mauvais workspace.

Divergence structurelle possible. Un environnement peut avoir des ressources que l'autre n'a pas (un WAF en prod, absent en dev) sans conditionnelle dans le code des modules.

Duplication du code racine. Chaque envs/<nom>/main.tf répète les mêmes appels de modules avec des paramètres différents. Cette duplication reste limitée si les modules concentrent la complexité, mais elle grandit avec le nombre d'environnements. Des outils tiers comme Terragrunt existent spécifiquement pour réduire cette duplication en générant les fichiers racine à partir d'un modèle commun.

Pas de garantie automatique de cohérence. Rien n'empêche envs/dev/main.tf et envs/prod/main.tf de diverger involontairement (un module ajouté dans un environnement et oublié dans l'autre). La discipline de revue de code est nécessaire pour éviter cette dérive.

Variables par environnement

Que ce soit avec des workspaces ou des répertoires séparés, les valeurs qui varient (taille d'instance, CIDR, nombre de réplicas) se regroupent généralement dans un fichier .tfvars par environnement plutôt que d'être codées en dur dans main.tf.

# envs/prod/prod.tfvars
instance_class = "db.r6g.large"
multi_az = true
vpc_cidr = "10.1.0.0/16"
# envs/dev/dev.tfvars
instance_class = "db.t4g.micro"
multi_az = false
vpc_cidr = "10.0.0.0/16"

Un fichier .tfvars nommé n'est pas chargé automatiquement : Terraform n'auto-charge que terraform.tfvars et les fichiers *.auto.tfvars. Tout autre fichier doit être passé explicitement à plan et apply :

terraform apply -var-file="prod.tfvars"

Cette approche fonctionne aussi bien avec des workspaces qu'avec des répertoires séparés. Avec des répertoires, chaque environnement contient son propre .tfvars. Avec des workspaces, il n'existe pas de sélection automatique du fichier selon terraform.workspace : le bon fichier doit être passé à la main, par exemple terraform apply -var-file="$(terraform workspace show).tfvars". Dans les deux cas, cette approche centralise les valeurs qui changent dans un seul fichier lisible, plutôt que dispersées dans des conditionnelles au milieu du code HCL.

Comparaison

CritèreWorkspacesRépertoires séparés
Isolation du stateOuiOui
Isolation de la configurationNon — même code pour tousOui — code indépendant par environnement
Comptes / credentials AWS distinctsNon — provider partagéOui — un provider par répertoire
Divergence structurelle entre environnementsDifficile (conditionnelles)Native
Risque d'erreur (mauvais environnement ciblé)Élevé (sélection implicite)Faible (répertoire explicite)
Duplication de codeAucuneBoilerplate racine dupliqué
Cas d'usage adaptéEnvironnements proches, projet modesteEnvironnements divergents, production critique

La structuration par répertoires est l'approche la plus répandue dès qu'un environnement de production est en jeu : l'isolation explicite réduit le risque d'appliquer une modification destinée à dev sur prod, et la possibilité de cibler des comptes AWS distincts limite l'ampleur d'une erreur. Les workspaces couvrent un besoin plus restreint — des environnements éphémères de test (une stack par pull request, par exemple), où la légèreté de terraform workspace new compense l'absence de divergence structurelle et le partage des credentials.