Terraform Workspaces : Gérer Plusieurs Environnements Efficacement

Gérez vos environnements dev, staging et prod avec les Terraform Workspaces. Création, sélection, variables conditionnelles par workspace et alternatives comme Terragrunt.

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.

Diagramme - Terraform Workspaces : Gérer Plusieurs Environnements Efficacement

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 apply dans le workspace prod au lieu de dev peut 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, prod plutôt que d, s, p.
  • Centralisez la configuration dans un bloc locals plutô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 !

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.