Lorsque vous travaillez avec Terraform en entreprise, vous devez inévitablement gérer plusieurs environnements : développement, staging, production, et parfois plus encore. Comment déployer la même infrastructure avec des configurations différentes selon l'environnement, sans dupliquer tout votre code ? C'est exactement le problème que résolvent les Terraform Workspaces.
Dans cet article, nous allons explorer en profondeur les workspaces Terraform : leur fonctionnement, leurs commandes, comment les intégrer dans votre code, leurs cas d'usage, leurs limites, et les alternatives disponibles. Nous construirons ensemble une configuration multi-environnement complète qui vous servira de base pour vos projets réels.

Qu'est-ce qu'un Workspace Terraform ?
Un workspace Terraform est un espace de travail isolé qui possède son propre fichier d'état (state). Concrètement, cela signifie que chaque workspace maintient une vue indépendante de l'infrastructure qu'il gère. Vous pouvez avoir le même code Terraform mais des ressources complètement différentes dans chaque workspace.
Pensez aux workspaces comme des branches Git, mais pour votre état d'infrastructure. Le code est le même, mais l'état — c'est-à-dire les ressources réellement créées — est différent dans chaque workspace.
Analogie : Imaginez un plan d'architecte (votre code Terraform) utilisé pour construire trois maisons identiques mais dans des quartiers différents (vos workspaces). Le plan est le même, mais chaque maison est indépendante et peut avoir des personnalisations mineures (taille du terrain, couleur, etc.).
Le workspace "default"
Lorsque vous initialisez un projet Terraform avec terraform init, vous travaillez automatiquement dans le workspace default. C'est le workspace créé par défaut et il ne peut pas être supprimé. Tant que vous ne créez pas d'autres workspaces, tout fonctionne comme d'habitude :
# Vérifier le workspace actif
terraform workspace show
# => default
Le fichier d'état du workspace default est stocké dans terraform.tfstate à la racine du projet (ou dans le backend configuré). Lorsque vous créez des workspaces supplémentaires, Terraform crée un sous-répertoire terraform.tfstate.d/ pour stocker leurs états respectifs.
Commandes de Gestion des Workspaces
Terraform fournit un ensemble de commandes pour gérer les workspaces. Maîtrisons-les une par une.
Créer un workspace
# Créer et basculer vers un nouveau workspace
terraform workspace new dev
# Created and switched to workspace "dev"!
terraform workspace new staging
# Created and switched to workspace "staging"!
terraform workspace new prod
# Created and switched to workspace "prod"!
La commande new crée le workspace ET bascule automatiquement dessus. Le fichier d'état de ce nouveau workspace est vide — aucune ressource n'est encore gérée.
Lister les workspaces
terraform workspace list
# default
# dev
# staging
# * prod
L'astérisque (*) indique le workspace actuellement actif. Dans cet exemple, nous sommes sur le workspace prod.
Sélectionner un workspace
# Basculer vers le workspace dev
terraform workspace select dev
# Switched to workspace "dev".
# Vérifier
terraform workspace show
# => dev
Supprimer un workspace
# Supprimer un workspace (doit être vide ou utiliser -force)
terraform workspace delete staging
# Deleted workspace "staging"!
# Forcer la suppression même si des ressources existent
terraform workspace delete -force staging
Attention : La suppression d'un workspace ne détruit pas les ressources qu'il gère. Si vous supprimez un workspace avec des ressources actives, celles-ci deviennent "orphelines" — elles existent toujours chez votre provider mais ne sont plus suivies par Terraform. Exécutez toujours terraform destroy avant de supprimer un workspace.
Utiliser terraform.workspace dans le Code
La variable intégrée terraform.workspace retourne le nom du workspace actif sous forme de chaîne de caractères. C'est le mécanisme principal pour adapter votre configuration selon l'environnement.
Nommage des ressources
# Les ressources sont automatiquement nommées selon le workspace
resource "aws_instance" "web" {
ami = "ami-0123456789abcdef0"
instance_type = var.instance_type
tags = {
Name = "web-server-${terraform.workspace}"
Environment = terraform.workspace
}
}
resource "aws_s3_bucket" "data" {
bucket = "monapp-data-${terraform.workspace}"
tags = {
Environment = terraform.workspace
}
}
Avec cette configuration, si vous êtes dans le workspace dev, l'instance s'appellera web-server-dev et le bucket monapp-data-dev. En prod, ce sera web-server-prod et monapp-data-prod.
Sélection conditionnelle de valeurs
Vous pouvez utiliser terraform.workspace dans des expressions conditionnelles pour adapter les configurations :
# Taille d'instance selon l'environnement
resource "aws_instance" "web" {
ami = "ami-0123456789abcdef0"
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
# Nombre d'instances
count = terraform.workspace == "prod" ? 3 : 1
tags = {
Name = "web-${terraform.workspace}-${count.index + 1}"
Environment = terraform.workspace
}
}
# Activer le multi-AZ uniquement en production
resource "aws_db_instance" "main" {
identifier = "db-${terraform.workspace}"
engine = "postgres"
engine_version = "16"
instance_class = terraform.workspace == "prod" ? "db.r6g.large" : "db.t3.micro"
multi_az = terraform.workspace == "prod" ? true : false
# Backup plus fréquent en production
backup_retention_period = terraform.workspace == "prod" ? 30 : 7
tags = {
Environment = terraform.workspace
}
}
Utilisation avec des maps de configuration
Pour une approche plus propre et maintenable, utilisez des maps (lookup tables) plutôt que des ternaires imbriqués :
# variables.tf
variable "instance_type" {
description = "Type d'instance par environnement"
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}
variable "instance_count" {
description = "Nombre d'instances par environnement"
type = map(number)
default = {
dev = 1
staging = 2
prod = 3
}
}
variable "enable_monitoring" {
description = "Activer le monitoring détaillé"
type = map(bool)
default = {
dev = false
staging = true
prod = true
}
}
# main.tf
resource "aws_instance" "web" {
count = lookup(var.instance_count, terraform.workspace, 1)
ami = data.aws_ami.amazon_linux.id
instance_type = lookup(var.instance_type, terraform.workspace, "t3.micro")
monitoring = lookup(var.enable_monitoring, terraform.workspace, false)
tags = {
Name = "web-${terraform.workspace}-${count.index + 1}"
Environment = terraform.workspace
}
}
La fonction lookup avec un troisième paramètre (valeur par défaut) permet de gérer gracieusement les workspaces non prévus dans la map.
Variables Conditionnelles par Workspace
Pour aller plus loin dans la personnalisation par workspace, vous pouvez utiliser un bloc locals qui centralise toute la configuration spécifique à chaque environnement :
# locals.tf
locals {
# Configuration centralisée par environnement
env_config = {
dev = {
instance_type = "t3.micro"
instance_count = 1
db_instance_class = "db.t3.micro"
db_multi_az = false
db_backup_retention = 7
enable_monitoring = false
enable_cdn = false
min_capacity = 1
max_capacity = 2
domain_prefix = "dev"
alert_email = "dev-team@example.com"
}
staging = {
instance_type = "t3.small"
instance_count = 2
db_instance_class = "db.t3.small"
db_multi_az = false
db_backup_retention = 14
enable_monitoring = true
enable_cdn = false
min_capacity = 2
max_capacity = 4
domain_prefix = "staging"
alert_email = "qa-team@example.com"
}
prod = {
instance_type = "t3.large"
instance_count = 3
db_instance_class = "db.r6g.large"
db_multi_az = true
db_backup_retention = 30
enable_monitoring = true
enable_cdn = true
min_capacity = 3
max_capacity = 10
domain_prefix = "www"
alert_email = "ops-team@example.com"
}
}
# Sélection de la configuration active
config = local.env_config[terraform.workspace]
# Tags communs
common_tags = {
Environment = terraform.workspace
Project = var.project_name
ManagedBy = "terraform"
Workspace = terraform.workspace
}
}
L'utilisation est ensuite très lisible :
# main.tf
resource "aws_instance" "web" {
count = local.config.instance_count
ami = data.aws_ami.amazon_linux.id
instance_type = local.config.instance_type
monitoring = local.config.enable_monitoring
tags = merge(local.common_tags, {
Name = "web-${terraform.workspace}-${count.index + 1}"
Role = "webserver"
})
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db-${terraform.workspace}"
engine = "postgres"
engine_version = "16"
instance_class = local.config.db_instance_class
multi_az = local.config.db_multi_az
backup_retention_period = local.config.db_backup_retention
tags = local.common_tags
}
resource "aws_cloudfront_distribution" "cdn" {
count = local.config.enable_cdn ? 1 : 0
# Configuration CDN...
enabled = true
origin {
domain_name = aws_lb.main.dns_name
origin_id = "alb"
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "alb"
viewer_protocol_policy = "redirect-to-https"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
tags = local.common_tags
}
Workspaces vs Répertoires Séparés
Il existe deux approches principales pour gérer plusieurs environnements avec Terraform. Chacune a ses avantages et inconvénients :
Approche Workspaces
# Un seul répertoire, plusieurs workspaces
mon-projet/
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf # Configuration par environnement
└── versions.tf
# Déployer en dev
terraform workspace select dev
terraform apply
# Déployer en prod
terraform workspace select prod
terraform apply
Avantages :
- Pas de duplication de code
- Changement rapide entre environnements
- Une seule source de vérité pour la configuration
- Simple à mettre en place
Inconvénients :
- Risque d'appliquer accidentellement sur le mauvais workspace
- Tous les environnements doivent partager la même structure
- Difficile de gérer des différences majeures entre environnements
- Un seul backend pour tous les environnements
Approche Répertoires Séparés
# Un répertoire par environnement
infrastructure/
├── modules/ # Modules partagés
│ ├── network/
│ ├── compute/
│ └── database/
├── environments/
│ ├── dev/
│ │ ├── main.tf # Appelle les modules
│ │ ├── variables.tf
│ │ ├── backend.tf # Backend spécifique
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ ├── backend.tf
│ └── terraform.tfvars
Avantages :
- Isolation totale entre environnements
- Pas de risque de confondre les environnements
- Backends séparés pour chaque environnement
- Chaque environnement peut évoluer indépendamment
- Permissions IAM distinctes possibles
Inconvénients :
- Duplication partielle du code (même si les modules réduisent ce problème)
- Plus de fichiers à maintenir
- Les modifications doivent être propagées manuellement
Quand utiliser quoi ?
Voici quelques recommandations :
- Utilisez les workspaces quand vos environnements sont structurellement identiques et ne diffèrent que par le dimensionnement (taille des instances, nombre de répliques, etc.).
- Utilisez les répertoires séparés quand vos environnements ont des différences architecturales significatives, quand vous avez besoin d'une isolation stricte (compliance, sécurité), ou quand différentes équipes gèrent différents environnements.
- Combinez les deux quand vous avez des environnements principaux très différents (répertoires) mais des sous-environnements similaires (workspaces pour dev-1, dev-2, etc.).
Cas d'Usage Concrets des Workspaces
Cas 1 : Environnements dev/staging/prod classiques
Le cas d'usage le plus courant. Chaque développeur ou équipe peut avoir son propre workspace de développement :
# Créer les environnements
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Chaque développeur peut aussi avoir son propre workspace
terraform workspace new dev-alice
terraform workspace new dev-bob
Cas 2 : Environnements éphémères pour les feature branches
Créez un environnement complet pour chaque feature branch, puis détruisez-le une fois la PR fusionnée :
# Script CI/CD pour créer un environnement de review
BRANCH_NAME=$(echo $CI_BRANCH | sed 's/[^a-zA-Z0-9]/-/g')
terraform workspace new "review-${BRANCH_NAME}" || \
terraform workspace select "review-${BRANCH_NAME}"
terraform apply -auto-approve \
-var="domain_prefix=review-${BRANCH_NAME}"
# Après merge de la PR
terraform workspace select "review-${BRANCH_NAME}"
terraform destroy -auto-approve
terraform workspace select default
terraform workspace delete "review-${BRANCH_NAME}"
Cas 3 : Multi-région
Utilisez les workspaces pour déployer la même infrastructure dans différentes régions :
# locals.tf
locals {
region_config = {
eu-west-3 = {
region = "eu-west-3"
azs = ["eu-west-3a", "eu-west-3b", "eu-west-3c"]
ami = "ami-0123456789abcdef0" # AMI Paris
}
us-east-1 = {
region = "us-east-1"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
ami = "ami-fedcba9876543210f" # AMI Virginia
}
ap-southeast-1 = {
region = "ap-southeast-1"
azs = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
ami = "ami-abcdef0123456789a" # AMI Singapore
}
}
config = local.region_config[terraform.workspace]
}
provider "aws" {
region = local.config.region
}
# Déployer en Europe
terraform workspace select eu-west-3
terraform apply
# Déployer aux États-Unis
terraform workspace select us-east-1
terraform apply
Cas 4 : Multi-tenant
Déployez une infrastructure par client dans une architecture SaaS :
# locals.tf
locals {
tenant_config = {
client-alpha = {
db_size = "db.t3.medium"
instance_count = 2
domain = "alpha.monapp.com"
features = ["basic", "reporting"]
}
client-beta = {
db_size = "db.r6g.large"
instance_count = 5
domain = "beta.monapp.com"
features = ["basic", "reporting", "api", "sso"]
}
client-gamma = {
db_size = "db.t3.small"
instance_count = 1
domain = "gamma.monapp.com"
features = ["basic"]
}
}
tenant = local.tenant_config[terraform.workspace]
}
Gestion du Backend avec les Workspaces
Lorsque vous utilisez un backend distant (S3, Azure Storage, GCS, etc.), les workspaces sont gérés automatiquement. Chaque workspace a son propre fichier d'état dans le backend :
# backend.tf
terraform {
backend "s3" {
bucket = "mon-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "eu-west-3"
encrypt = true
dynamodb_table = "terraform-locks"
# Les workspaces créeront des clés comme :
# env:/dev/infrastructure/terraform.tfstate
# env:/staging/infrastructure/terraform.tfstate
# env:/prod/infrastructure/terraform.tfstate
}
}
Terraform préfixe automatiquement la clé avec env:/<workspace_name>/ pour chaque workspace non-default. Le workspace default utilise la clé telle quelle.
Vous pouvez personnaliser ce comportement avec le paramètre workspace_key_prefix :
terraform {
backend "s3" {
bucket = "mon-terraform-state"
key = "terraform.tfstate"
region = "eu-west-3"
workspace_key_prefix = "environments"
# Résultat :
# environments/dev/terraform.tfstate
# environments/staging/terraform.tfstate
# environments/prod/terraform.tfstate
}
}
Exemple Pratique Complet : Configuration Multi-Environnement
Voici un exemple complet et production-ready d'une infrastructure multi-environnement utilisant les workspaces. Ce projet déploie une application web avec un load balancer, des instances EC2, une base de données RDS et un cache ElastiCache.
Structure du projet
multi-env-project/
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf
├── versions.tf
├── network.tf
├── compute.tf
├── database.tf
├── monitoring.tf
└── backend.tf
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
variables.tf
variable "project_name" {
description = "Nom du projet"
type = string
default = "monapp"
}
variable "aws_region" {
description = "Région AWS"
type = string
default = "eu-west-3"
}
variable "allowed_ips" {
description = "IPs autorisées pour l'accès SSH"
type = list(string)
default = []
}
locals.tf
locals {
# ==========================================
# Configuration par environnement
# ==========================================
env_config = {
dev = {
# Réseau
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
# Compute
instance_type = "t3.micro"
instance_count = 1
min_size = 1
max_size = 2
# Base de données
db_instance_class = "db.t3.micro"
db_allocated_storage = 20
db_multi_az = false
db_backup_retention = 7
db_deletion_protection = false
# Cache
cache_node_type = "cache.t3.micro"
cache_num_nodes = 1
# Monitoring
enable_detailed_monitoring = false
alarm_evaluation_periods = 3
alarm_period = 300
# DNS
domain_prefix = "dev"
}
staging = {
vpc_cidr = "10.1.0.0/16"
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24"]
private_subnet_cidrs = ["10.1.10.0/24", "10.1.20.0/24"]
instance_type = "t3.small"
instance_count = 2
min_size = 2
max_size = 4
db_instance_class = "db.t3.small"
db_allocated_storage = 50
db_multi_az = false
db_backup_retention = 14
db_deletion_protection = false
cache_node_type = "cache.t3.small"
cache_num_nodes = 2
enable_detailed_monitoring = true
alarm_evaluation_periods = 2
alarm_period = 300
domain_prefix = "staging"
}
prod = {
vpc_cidr = "10.2.0.0/16"
public_subnet_cidrs = ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]
private_subnet_cidrs = ["10.2.10.0/24", "10.2.20.0/24", "10.2.30.0/24"]
instance_type = "t3.large"
instance_count = 3
min_size = 3
max_size = 10
db_instance_class = "db.r6g.large"
db_allocated_storage = 100
db_multi_az = true
db_backup_retention = 30
db_deletion_protection = true
cache_node_type = "cache.r6g.large"
cache_num_nodes = 3
enable_detailed_monitoring = true
alarm_evaluation_periods = 1
alarm_period = 60
domain_prefix = "www"
}
}
# Configuration active basée sur le workspace
config = local.env_config[terraform.workspace]
# Tags communs
common_tags = {
Project = var.project_name
Environment = terraform.workspace
ManagedBy = "terraform"
}
# Zones de disponibilité
azs = slice(
data.aws_availability_zones.available.names,
0,
length(local.config.public_subnet_cidrs)
)
}
data "aws_availability_zones" "available" {
state = "available"
}
network.tf
# ==========================================
# VPC
# ==========================================
resource "aws_vpc" "main" {
cidr_block = local.config.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${terraform.workspace}-vpc"
}
}
# ==========================================
# Sous-réseaux publics
# ==========================================
resource "aws_subnet" "public" {
count = length(local.config.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = local.config.public_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${terraform.workspace}-public-${count.index + 1}"
Type = "Public"
}
}
# ==========================================
# Sous-réseaux privés
# ==========================================
resource "aws_subnet" "private" {
count = length(local.config.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = local.config.private_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project_name}-${terraform.workspace}-private-${count.index + 1}"
Type = "Private"
}
}
# ==========================================
# Internet Gateway
# ==========================================
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${terraform.workspace}-igw"
}
}
# ==========================================
# NAT Gateway (une seule en dev/staging, une par AZ en prod)
# ==========================================
resource "aws_eip" "nat" {
count = terraform.workspace == "prod" ? length(local.azs) : 1
domain = "vpc"
tags = {
Name = "${var.project_name}-${terraform.workspace}-nat-eip-${count.index + 1}"
}
}
resource "aws_nat_gateway" "main" {
count = terraform.workspace == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${terraform.workspace}-nat-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
# ==========================================
# Tables de routage
# ==========================================
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.config.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
count = length(local.config.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[
terraform.workspace == "prod" ? count.index : 0
].id
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-private-rt-${count.index + 1}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.config.private_subnet_cidrs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
compute.tf
# ==========================================
# Security Group pour le Load Balancer
# ==========================================
resource "aws_security_group" "alb" {
name_prefix = "${var.project_name}-${terraform.workspace}-alb-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-alb-sg"
}
lifecycle {
create_before_destroy = true
}
}
# ==========================================
# Security Group pour les instances
# ==========================================
resource "aws_security_group" "app" {
name_prefix = "${var.project_name}-${terraform.workspace}-app-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-app-sg"
}
lifecycle {
create_before_destroy = true
}
}
# ==========================================
# Application Load Balancer
# ==========================================
resource "aws_lb" "main" {
name = "${var.project_name}-${terraform.workspace}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
tags = {
Name = "${var.project_name}-${terraform.workspace}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-${terraform.workspace}-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 10
timeout = 5
interval = 30
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-tg"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
# ==========================================
# Launch Template
# ==========================================
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_launch_template" "app" {
name_prefix = "${var.project_name}-${terraform.workspace}-"
image_id = data.aws_ami.amazon_linux.id
instance_type = local.config.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
monitoring {
enabled = local.config.enable_detailed_monitoring
}
user_data = base64encode(templatefile("${path.module}/scripts/user_data.sh", {
environment = terraform.workspace
project_name = var.project_name
}))
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, {
Name = "${var.project_name}-${terraform.workspace}-app"
})
}
lifecycle {
create_before_destroy = true
}
}
# ==========================================
# Auto Scaling Group
# ==========================================
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-${terraform.workspace}-asg"
vpc_zone_identifier = aws_subnet.private[*].id
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = local.config.min_size
max_size = local.config.max_size
desired_capacity = local.config.instance_count
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "${var.project_name}-${terraform.workspace}-app"
propagate_at_launch = true
}
}
database.tf
# ==========================================
# Security Group pour RDS
# ==========================================
resource "aws_security_group" "db" {
name_prefix = "${var.project_name}-${terraform.workspace}-db-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "${var.project_name}-${terraform.workspace}-db-sg"
}
lifecycle {
create_before_destroy = true
}
}
# ==========================================
# DB Subnet Group
# ==========================================
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${terraform.workspace}-db-subnet"
subnet_ids = aws_subnet.private[*].id
tags = {
Name = "${var.project_name}-${terraform.workspace}-db-subnet"
}
}
# ==========================================
# RDS PostgreSQL
# ==========================================
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${terraform.workspace}-db"
engine = "postgres"
engine_version = "16"
instance_class = local.config.db_instance_class
allocated_storage = local.config.db_allocated_storage
max_allocated_storage = local.config.db_allocated_storage * 2
storage_encrypted = true
db_name = "${var.project_name}_${terraform.workspace}"
username = "${var.project_name}_admin"
password = var.db_password
multi_az = local.config.db_multi_az
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
backup_retention_period = local.config.db_backup_retention
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
deletion_protection = local.config.db_deletion_protection
skip_final_snapshot = terraform.workspace != "prod"
final_snapshot_identifier = terraform.workspace == "prod" ? "${var.project_name}-prod-final-snapshot" : null
tags = {
Name = "${var.project_name}-${terraform.workspace}-db"
}
}
outputs.tf
output "environment" {
description = "Environnement actif"
value = terraform.workspace
}
output "vpc_id" {
description = "ID du VPC"
value = aws_vpc.main.id
}
output "alb_dns_name" {
description = "DNS du load balancer"
value = aws_lb.main.dns_name
}
output "db_endpoint" {
description = "Endpoint de la base de données"
value = aws_db_instance.main.endpoint
}
output "app_url" {
description = "URL de l'application"
value = "http://${aws_lb.main.dns_name}"
}
output "infrastructure_summary" {
description = "Résumé de l'infrastructure déployée"
value = {
environment = terraform.workspace
vpc_cidr = aws_vpc.main.cidr_block
instance_type = local.config.instance_type
instance_count = local.config.instance_count
db_class = local.config.db_instance_class
db_multi_az = local.config.db_multi_az
monitoring = local.config.enable_detailed_monitoring
}
}
Déploiement complet
# Initialiser le projet
terraform init
# ---- Déployer l'environnement de développement ----
terraform workspace new dev
terraform plan -var="db_password=dev-password-123"
terraform apply -var="db_password=dev-password-123"
# Vérifier l'infrastructure
terraform output infrastructure_summary
# ---- Déployer le staging ----
terraform workspace new staging
terraform plan -var="db_password=staging-password-456"
terraform apply -var="db_password=staging-password-456"
# ---- Déployer la production ----
terraform workspace new prod
terraform plan -var="db_password=prod-super-secure-789"
terraform apply -var="db_password=prod-super-secure-789"
# Basculer entre les environnements
terraform workspace select dev
terraform output app_url
terraform workspace select prod
terraform output app_url
Limites des Workspaces
Bien que les workspaces soient un outil puissant, ils présentent certaines limitations importantes à connaître :
- Pas d'isolation de backend : Tous les workspaces partagent le même backend. Si le backend est compromis ou indisponible, tous les environnements sont affectés.
- Risque d'erreur humaine : Il est facile d'oublier de changer de workspace et d'appliquer des modifications sur le mauvais environnement. Un
terraform applydans le workspaceprodau lieu dedevpeut avoir des conséquences désastreuses. - Pas d'isolation des credentials : Les mêmes credentials sont utilisées pour tous les workspaces. Il n'est pas possible de restreindre l'accès au workspace prod sans restreindre l'accès aux autres.
- Structure identique obligatoire : Tous les workspaces exécutent le même code. Si la production nécessite des ressources que le développement n'a pas, vous devez utiliser des conditions (
count,for_each), ce qui peut devenir complexe. - Pas de protection native : Terraform n'offre pas de mécanisme intégré pour protéger un workspace contre les modifications accidentelles (pas de "prod lock").
- Visibilité limitée : Il n'y a pas de tableau de bord natif pour voir l'état de tous les workspaces simultanément.
Alternatives aux Workspaces
Selon vos besoins, d'autres solutions peuvent être plus adaptées que les workspaces natifs :
Terragrunt
Terragrunt est un wrapper autour de Terraform développé par Gruntwork. Il résout plusieurs limitations des workspaces :
# Structure Terragrunt
infrastructure/
├── terragrunt.hcl # Configuration parente
├── dev/
│ ├── terragrunt.hcl # Hérite de la config parente
│ └── env.hcl # Variables spécifiques
├── staging/
│ ├── terragrunt.hcl
│ └── env.hcl
└── prod/
├── terragrunt.hcl
└── env.hcl
# prod/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/infrastructure"
}
inputs = {
environment = "prod"
instance_type = "t3.large"
instance_count = 3
db_multi_az = true
}
Terragrunt offre une isolation complète des backends, la gestion des dépendances entre modules, et des fonctions DRY pour éviter la répétition.
Répertoires avec Makefiles
Une approche simple mais efficace consiste à utiliser des répertoires séparés avec un Makefile pour automatiser les déploiements :
# Makefile
ENV ?= dev
plan:
cd environments/$(ENV) && terraform plan
apply:
cd environments/$(ENV) && terraform apply
destroy:
cd environments/$(ENV) && terraform destroy
# Utilisation
make plan ENV=dev
make apply ENV=prod
Terraform Cloud / Enterprise
Terraform Cloud (gratuit pour les petites équipes) et Terraform Enterprise (version payante) offrent une gestion avancée des workspaces avec :
- Interface web pour visualiser tous les workspaces
- Contrôle d'accès granulaire (RBAC)
- Approbation manuelle avant les apply en production
- Exécution distante des plans et applies
- Intégration avec les systèmes de VCS (GitHub, GitLab, etc.)
- Politique as Code avec Sentinel
Bonnes Pratiques pour les Workspaces
Si vous choisissez d'utiliser les workspaces, voici les bonnes pratiques à suivre :
- Affichez toujours le workspace actif dans votre prompt shell pour éviter les erreurs.
- Utilisez un script wrapper qui affiche le workspace avant chaque apply et demande confirmation.
- Nommez vos workspaces de manière claire et cohérente :
dev,staging,prodplutôt qued,s,p. - Centralisez la configuration dans un bloc
localsplutôt que de disperser les ternaires dans tout le code. - Validez le workspace au début de votre configuration pour détecter les erreurs tôt.
- Protégez la production en ajoutant des validations supplémentaires dans vos scripts CI/CD.
# Validation du workspace dans locals.tf
locals {
valid_workspaces = ["dev", "staging", "prod"]
# Cette ligne provoquera une erreur si le workspace n'est pas valide
_validate_workspace = index(local.valid_workspaces, terraform.workspace)
}
# Script wrapper pour terraform apply
#!/bin/bash
WORKSPACE=$(terraform workspace show)
echo "================================"
echo "WORKSPACE ACTIF : $WORKSPACE"
echo "================================"
if [ "$WORKSPACE" = "prod" ]; then
echo "ATTENTION : Vous êtes en PRODUCTION !"
read -p "Êtes-vous sûr de vouloir continuer ? (oui/non) " confirm
if [ "$confirm" != "oui" ]; then
echo "Opération annulée."
exit 1
fi
fi
terraform apply "$@"
Conclusion
Les Terraform Workspaces sont un outil précieux pour gérer plusieurs environnements à partir d'une même base de code. Ils brillent particulièrement lorsque vos environnements sont structurellement identiques et ne diffèrent que par le dimensionnement. Pour des besoins plus complexes — isolation stricte, permissions différenciées, architectures divergentes — les répertoires séparés ou des outils comme Terragrunt peuvent être plus appropriés.
L'essentiel est de choisir la bonne approche pour votre contexte : la taille de votre équipe, vos exigences de sécurité, la complexité de vos environnements, et votre niveau de maturité avec Terraform. Quelle que soit l'approche choisie, l'important est d'avoir une stratégie claire et documentée pour la gestion de vos environnements.
Avec cet article, vous disposez maintenant de toutes les connaissances nécessaires pour implémenter une gestion multi-environnement robuste avec Terraform. N'hésitez pas à expérimenter avec les workspaces dans un projet de test avant de les adopter en production. Et surtout, pensez toujours à vérifier votre workspace actif avant chaque terraform apply !