Les Variables et Outputs Terraform : Rendre votre Code Flexible et Réutilisable

Apprenez à utiliser les variables et outputs Terraform pour rendre votre code flexible et réutilisable. Variables d'entrée, fichiers .tfvars, validation, outputs et locals expliqués.

L'un des principes fondamentaux de l'Infrastructure as Code est la réutilisabilité. Écrire du code Terraform en dur avec des valeurs figées, c'est comme écrire un programme sans paramètres : ça fonctionne une fois, mais c'est impossible à maintenir ou à adapter. C'est là qu'interviennent les variables, les outputs et les locals de Terraform. Ces mécanismes transforment vos configurations statiques en templates dynamiques, capables de s'adapter à n'importe quel environnement — développement, staging ou production — sans modifier une seule ligne de code HCL.

Dans cet article, nous allons explorer en profondeur le système de variables de Terraform. Vous apprendrez à déclarer des variables d'entrée avec des types, des validations et des valeurs par défaut, à utiliser les fichiers .tfvars et les variables d'environnement, à exposer des informations via les outputs, et à organiser le tout selon les bonnes pratiques. Préparez-vous à rendre votre code Terraform véritablement flexible et professionnel.

Flux des Variables Terraform

Diagramme - Les Variables et Outputs Terraform : Rendre votre Code Flexible et Réutilisable

Les Variables d'Entrée (Input Variables)

Déclarer une variable simple

Une variable d'entrée se déclare avec le bloc variable. À sa forme la plus simple, elle ne nécessite qu'un nom :

variable "region" {}

Cependant, en pratique, vous voudrez toujours fournir au minimum une description et un type. Voici une déclaration plus complète :

variable "region" {
  description = "La région AWS dans laquelle déployer les ressources"
  type        = string
  default     = "eu-west-3"
}

Les attributs principaux d'une variable sont :

  • description : une explication lisible de la variable, affichée lors du terraform plan et dans la documentation générée.
  • type : le type de données attendu (nous y reviendrons en détail).
  • default : la valeur par défaut. Si elle est définie, la variable devient optionnelle.
  • sensitive : un booléen qui masque la valeur dans les logs.
  • nullable : indique si la variable peut accepter null comme valeur.
  • validation : un ou plusieurs blocs de validation personnalisée.

Les types de variables

Terraform supporte un système de types riche qui vous permet de contraindre précisément les valeurs acceptées. Voici les types disponibles :

Types primitifs :

# String - chaîne de caractères
variable "environment" {
  description = "Nom de l'environnement"
  type        = string
  default     = "development"
}

# Number - nombre entier ou décimal
variable "instance_count" {
  description = "Nombre d'instances à créer"
  type        = number
  default     = 2
}

# Bool - booléen
variable "enable_monitoring" {
  description = "Activer le monitoring CloudWatch"
  type        = bool
  default     = true
}

Types complexes — collections :

# List - liste ordonnée d'éléments du même type
variable "availability_zones" {
  description = "Liste des zones de disponibilité"
  type        = list(string)
  default     = ["eu-west-3a", "eu-west-3b", "eu-west-3c"]
}

# Map - paires clé/valeur
variable "tags" {
  description = "Tags à appliquer aux ressources"
  type        = map(string)
  default = {
    Project     = "mon-projet"
    ManagedBy   = "terraform"
  }
}

# Set - ensemble non ordonné d'éléments uniques
variable "allowed_ports" {
  description = "Ports autorisés dans le security group"
  type        = set(number)
  default     = [22, 80, 443]
}

Types complexes — structurels :

# Object - structure avec des attributs nommés et typés
variable "database_config" {
  description = "Configuration de la base de données"
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
    multi_az       = bool
  })
  default = {
    engine            = "postgres"
    engine_version    = "14.3"
    instance_class    = "db.t3.micro"
    allocated_storage = 20
    multi_az          = false
  }
}

# Tuple - liste ordonnée avec des types différents par position
variable "ingress_rule" {
  description = "Règle d'entrée : [port, protocole, cidr]"
  type        = tuple([number, string, string])
  default     = [443, "tcp", "0.0.0.0/0"]
}

Le type object est particulièrement puissant pour regrouper des configurations liées en une seule variable cohérente, plutôt que de multiplier les variables individuelles.

Validation des variables

Depuis Terraform 0.13, vous pouvez ajouter des blocs validation à vos variables pour vérifier que les valeurs fournies respectent vos contraintes métier. C'est un filet de sécurité essentiel :

variable "environment" {
  description = "Environnement de déploiement"
  type        = string

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "L'environnement doit être 'development', 'staging' ou 'production'."
  }
}

variable "instance_type" {
  description = "Type d'instance EC2"
  type        = string
  default     = "t3.micro"

  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "Seules les instances de la famille t3 sont autorisées."
  }
}

variable "cidr_block" {
  description = "Bloc CIDR du VPC"
  type        = string
  default     = "10.0.0.0/16"

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "La valeur doit être un bloc CIDR valide (ex: 10.0.0.0/16)."
  }
}

Vous pouvez même combiner plusieurs blocs validation sur une même variable pour vérifier différentes contraintes :

variable "project_name" {
  description = "Nom du projet"
  type        = string

  validation {
    condition     = length(var.project_name) >= 3
    error_message = "Le nom du projet doit contenir au moins 3 caractères."
  }

  validation {
    condition     = length(var.project_name) <= 24
    error_message = "Le nom du projet ne doit pas dépasser 24 caractères."
  }

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]*$", var.project_name))
    error_message = "Le nom du projet doit commencer par une lettre minuscule et ne contenir que des lettres minuscules, des chiffres et des tirets."
  }
}

Variables sensibles

Certaines variables contiennent des informations confidentielles : mots de passe, clés API, tokens. Terraform permet de les marquer comme sensitive pour éviter qu'elles apparaissent dans les logs et les sorties du plan :

variable "db_password" {
  description = "Mot de passe de la base de données"
  type        = string
  sensitive   = true

  validation {
    condition     = length(var.db_password) >= 12
    error_message = "Le mot de passe doit contenir au moins 12 caractères."
  }
}

variable "api_key" {
  description = "Clé API du service externe"
  type        = string
  sensitive   = true
}

Lorsque vous exécutez terraform plan ou terraform apply, les valeurs sensibles sont remplacées par (sensitive value) dans toute la sortie. Attention toutefois : la valeur reste stockée en clair dans le fichier state. La sécurisation du state est donc cruciale (nous y reviendrons dans un prochain article).

Fournir des Valeurs aux Variables

Terraform offre plusieurs mécanismes pour passer des valeurs aux variables, avec un ordre de priorité bien défini.

Fichiers .tfvars

La méthode la plus courante est d'utiliser des fichiers .tfvars. Ces fichiers contiennent les valeurs assignées aux variables :

# terraform.tfvars
region         = "eu-west-3"
environment    = "production"
instance_count = 3
instance_type  = "t3.medium"

tags = {
  Project     = "mon-application"
  Team        = "platform"
  ManagedBy   = "terraform"
}

availability_zones = [
  "eu-west-3a",
  "eu-west-3b",
  "eu-west-3c"
]

database_config = {
  engine            = "postgres"
  engine_version    = "14.3"
  instance_class    = "db.r5.large"
  allocated_storage = 100
  multi_az          = true
}

Par convention, Terraform charge automatiquement les fichiers nommés terraform.tfvars ou *.auto.tfvars. Pour tout autre nom, vous devez le spécifier explicitement :

# Chargement automatique
terraform apply  # charge terraform.tfvars et *.auto.tfvars

# Chargement explicite
terraform apply -var-file="production.tfvars"
terraform apply -var-file="environments/staging.tfvars"

Cette mécanique permet de gérer facilement plusieurs environnements :

project/
├── main.tf
├── variables.tf
├── outputs.tf
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── production.tfvars
# Déployer en dev
terraform plan -var-file="environments/dev.tfvars"

# Déployer en production
terraform plan -var-file="environments/production.tfvars"

Variables d'environnement TF_VAR_

Terraform lit automatiquement les variables d'environnement préfixées par TF_VAR_. Le nom de la variable d'environnement correspond au nom de la variable Terraform en majuscules après le préfixe :

# Définir des variables via l'environnement
export TF_VAR_region="eu-west-3"
export TF_VAR_environment="staging"
export TF_VAR_instance_count=2
export TF_VAR_db_password="MonSuperMotDePasse123!"

# Puis exécuter terraform normalement
terraform plan
terraform apply

Cette méthode est particulièrement adaptée pour les variables sensibles dans les pipelines CI/CD. Au lieu de stocker un mot de passe dans un fichier, vous le passez via une variable d'environnement injectée par votre système de secrets (Vault, AWS Secrets Manager, etc.).

Ligne de commande avec -var

Vous pouvez aussi passer des valeurs directement en ligne de commande :

terraform apply \
  -var="region=eu-west-3" \
  -var="environment=production" \
  -var='tags={"Project":"web","Team":"devops"}'

Ordre de priorité

Lorsque plusieurs sources fournissent la même variable, Terraform applique l'ordre de priorité suivant (du moins prioritaire au plus prioritaire) :

  1. Valeur par défaut dans la déclaration variable
  2. Variables d'environnement TF_VAR_*
  3. Fichier terraform.tfvars (chargé automatiquement)
  4. Fichiers *.auto.tfvars (chargés automatiquement, par ordre alphabétique)
  5. Fichiers spécifiés avec -var-file (dans l'ordre de la ligne de commande)
  6. Options -var en ligne de commande (dans l'ordre)

Cet ordre est déterministe : la dernière valeur trouvée l'emporte toujours.

Les Outputs (Valeurs de Sortie)

Déclarer et utiliser des outputs

Les outputs permettent d'exposer des informations sur votre infrastructure après un terraform apply. Ils sont déclarés avec le bloc output :

output "vpc_id" {
  description = "L'identifiant du VPC créé"
  value       = aws_vpc.main.id
}

output "instance_public_ip" {
  description = "L'adresse IP publique de l'instance EC2"
  value       = aws_instance.web.public_ip
}

output "database_endpoint" {
  description = "Le point de terminaison de la base de données RDS"
  value       = aws_db_instance.main.endpoint
}

output "load_balancer_dns" {
  description = "Le nom DNS du load balancer"
  value       = aws_lb.main.dns_name
}

Après un terraform apply, ces valeurs sont affichées dans le terminal :

Apply complete! Resources: 12 added, 0 changed, 0 destroyed.

Outputs:

database_endpoint = "mydb.abc123.eu-west-3.rds.amazonaws.com:5432"
instance_public_ip = "52.47.123.45"
load_balancer_dns = "my-lb-1234567890.eu-west-3.elb.amazonaws.com"
vpc_id = "vpc-0abc123def456"

Vous pouvez également consulter les outputs à tout moment avec :

# Afficher tous les outputs
terraform output

# Afficher un output spécifique
terraform output vpc_id

# Afficher un output en format JSON (utile pour le scripting)
terraform output -json

Outputs sensibles

Comme les variables, les outputs peuvent être marqués comme sensibles :

output "db_password" {
  description = "Le mot de passe généré pour la base de données"
  value       = random_password.db.result
  sensitive   = true
}

output "tls_private_key" {
  description = "La clé privée TLS"
  value       = tls_private_key.main.private_key_pem
  sensitive   = true
}

Un output sensible n'est pas affiché dans le terminal, mais reste accessible via terraform output -json ou terraform output db_password.

Outputs et dépendances entre modules

L'un des usages les plus importants des outputs est le partage de données entre modules. Lorsque vous structurez votre code en modules (ce que nous verrons dans un futur article), les outputs du module enfant sont accessibles par le module parent :

# Module réseau - outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

# Module applicatif - main.tf
module "network" {
  source = "./modules/network"
  # ...
}

resource "aws_instance" "web" {
  subnet_id = module.network.public_subnet_ids[0]
  vpc_security_group_ids = [aws_security_group.web.id]
  # ...
}

Les outputs créent ainsi un contrat d'interface clair entre les différents composants de votre infrastructure.

Outputs avec des expressions complexes

Les outputs peuvent contenir des expressions élaborées pour formater ou transformer les données exposées :

output "instance_info" {
  description = "Informations détaillées sur les instances"
  value = {
    for instance in aws_instance.web :
    instance.tags["Name"] => {
      id         = instance.id
      public_ip  = instance.public_ip
      private_ip = instance.private_ip
      az         = instance.availability_zone
    }
  }
}

output "connection_strings" {
  description = "Chaînes de connexion formatées"
  value = {
    postgres = "postgresql://${var.db_user}:****@${aws_db_instance.main.endpoint}/${var.db_name}"
    redis    = "redis://${aws_elasticache_cluster.main.cache_nodes[0].address}:6379"
  }
}

Les Locals (Variables Locales)

Les locals sont des variables internes à votre configuration. Contrairement aux variables d'entrée, elles ne sont pas exposées à l'utilisateur. Elles servent à simplifier et centraliser des expressions complexes ou des valeurs calculées :

locals {
  # Valeurs calculées
  project_prefix = "${var.project_name}-${var.environment}"

  # Tags communs à toutes les ressources
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
    Region      = var.region
    CreatedAt   = timestamp()
  }

  # Logique conditionnelle centralisée
  is_production    = var.environment == "production"
  instance_type    = local.is_production ? "t3.large" : "t3.micro"
  instance_count   = local.is_production ? 3 : 1
  multi_az         = local.is_production ? true : false
  enable_backups   = local.is_production ? true : false

  # Calculs de sous-réseaux
  azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
  public_subnets = [
    for i, az in local.azs :
    cidrsubnet(var.vpc_cidr, 8, i)
  ]
  private_subnets = [
    for i, az in local.azs :
    cidrsubnet(var.vpc_cidr, 8, i + 10)
  ]
}

Ensuite, vous référencez ces locals avec le préfixe local. (au singulier) :

resource "aws_instance" "web" {
  count         = local.instance_count
  instance_type = local.instance_type
  ami           = data.aws_ami.amazon_linux.id

  tags = merge(local.common_tags, {
    Name = "${local.project_prefix}-web-${count.index + 1}"
    Role = "web-server"
  })
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr

  tags = merge(local.common_tags, {
    Name = "${local.project_prefix}-vpc"
  })
}

Les locals sont particulièrement utiles pour :

  • Éviter de répéter des expressions complexes à plusieurs endroits
  • Centraliser la logique conditionnelle liée à l'environnement
  • Créer des tags cohérents appliqués à toutes les ressources
  • Préparer des données transformées (calculs CIDR, formatage de noms, etc.)

Bonnes Pratiques d'Organisation des Variables

Structure de fichiers recommandée

Par convention, les variables, outputs et locals sont organisés dans des fichiers dédiés :

project/
├── main.tf           # Ressources principales
├── variables.tf      # Déclarations de variables
├── outputs.tf        # Déclarations des outputs
├── locals.tf         # Variables locales
├── providers.tf      # Configuration des providers
├── versions.tf       # Contraintes de versions
├── terraform.tfvars  # Valeurs par défaut (non commité si sensible)
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── prod.tfvars
└── .gitignore        # Exclure *.tfvars sensibles

Conventions de nommage

Adoptez des conventions de nommage claires et cohérentes :

# BON : noms descriptifs avec des underscores
variable "vpc_cidr_block" {}
variable "enable_dns_support" {}
variable "max_instance_count" {}

# MAUVAIS : noms ambigus ou incohérents
variable "cidr" {}        # Trop vague
variable "enableDns" {}   # camelCase non conventionnel
variable "cnt" {}         # Abréviation obscure

Grouper les variables par catégorie

Dans votre fichier variables.tf, regroupez les variables par thème avec des commentaires :

# ==============================================================================
# Variables Générales
# ==============================================================================

variable "project_name" {
  description = "Nom du projet utilisé pour le nommage des ressources"
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,23}$", var.project_name))
    error_message = "Le nom du projet doit faire entre 3 et 24 caractères (minuscules, chiffres, tirets)."
  }
}

variable "environment" {
  description = "Environnement de déploiement"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "L'environnement doit être 'dev', 'staging' ou 'prod'."
  }
}

variable "region" {
  description = "Région AWS cible"
  type        = string
  default     = "eu-west-3"
}

# ==============================================================================
# Variables Réseau
# ==============================================================================

variable "vpc_cidr" {
  description = "Bloc CIDR pour le VPC"
  type        = string
  default     = "10.0.0.0/16"

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Veuillez fournir un bloc CIDR valide."
  }
}

variable "enable_nat_gateway" {
  description = "Créer une NAT Gateway pour les sous-réseaux privés"
  type        = bool
  default     = false
}

# ==============================================================================
# Variables Compute
# ==============================================================================

variable "instance_type" {
  description = "Type d'instance EC2"
  type        = string
  default     = "t3.micro"
}

variable "instance_count" {
  description = "Nombre d'instances à déployer"
  type        = number
  default     = 1

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "Le nombre d'instances doit être compris entre 1 et 10."
  }
}

# ==============================================================================
# Variables Base de Données
# ==============================================================================

variable "db_config" {
  description = "Configuration de la base de données RDS"
  type = object({
    engine            = string
    engine_version    = string
    instance_class    = string
    allocated_storage = number
    multi_az          = bool
    backup_retention  = number
  })
  default = {
    engine            = "postgres"
    engine_version    = "14.3"
    instance_class    = "db.t3.micro"
    allocated_storage = 20
    multi_az          = false
    backup_retention  = 7
  }
}

variable "db_password" {
  description = "Mot de passe administrateur de la base de données"
  type        = string
  sensitive   = true

  validation {
    condition     = length(var.db_password) >= 16
    error_message = "Le mot de passe doit contenir au moins 16 caractères pour la sécurité."
  }
}

Documenter et valider systématiquement

Chaque variable devrait avoir :

  • Une description claire et précise
  • Un type explicite (même si Terraform peut inférer)
  • Une valeur par défaut raisonnable quand c'est pertinent
  • Des validations pour les contraintes métier

Le fichier .gitignore pour les variables

Ne commettez jamais de fichiers contenant des valeurs sensibles. Voici un .gitignore type pour un projet Terraform :

# Fichiers Terraform locaux
.terraform/
*.tfstate
*.tfstate.backup

# Fichiers de variables potentiellement sensibles
# Commiter uniquement les .tfvars non sensibles
secret.tfvars
*.secret.tfvars

# Fichiers de plan
*.tfplan

# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Crash logs
crash.log
crash.*.log

Exemple Pratique Complet

Pour conclure avec un exemple concret, voici une configuration complète qui illustre l'utilisation combinée des variables, locals et outputs pour un déploiement web :

# variables.tf
variable "project_name" {
  description = "Nom du projet"
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,23}$", var.project_name))
    error_message = "Nom invalide : 3-24 caractères, minuscules, chiffres et tirets."
  }
}

variable "environment" {
  description = "Environnement cible"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environnement invalide."
  }
}

variable "web_config" {
  description = "Configuration du serveur web"
  type = object({
    instance_type = string
    min_size      = number
    max_size      = number
    port          = number
  })
  default = {
    instance_type = "t3.micro"
    min_size      = 1
    max_size      = 3
    port          = 8080
  }
}
# locals.tf
locals {
  name_prefix = "${var.project_name}-${var.environment}"

  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }

  is_prod      = var.environment == "prod"
  web_min_size = local.is_prod ? max(var.web_config.min_size, 2) : 1
  web_max_size = local.is_prod ? var.web_config.max_size : 2
}
# outputs.tf
output "deployment_summary" {
  description = "Résumé du déploiement"
  value = {
    project     = var.project_name
    environment = var.environment
    region      = var.region
    is_prod     = local.is_prod
    web_scaling = "${local.web_min_size}-${local.web_max_size} instances"
  }
}

Conclusion

Les variables, outputs et locals constituent le socle de tout code Terraform professionnel. En maîtrisant ces mécanismes, vous transformez des configurations rigides en templates réutilisables, capables de s'adapter à tous vos environnements.

Retenez ces principes essentiels :

  • Typez et validez systématiquement vos variables d'entrée
  • Utilisez les locals pour centraliser la logique et éviter les répétitions
  • Exposez via les outputs les informations nécessaires aux autres modules et aux utilisateurs
  • Protégez les données sensibles avec l'attribut sensitive et les variables d'environnement
  • Organisez vos fichiers selon les conventions de la communauté Terraform

Dans le prochain article, nous mettrons ces connaissances en pratique en déployant vos premières ressources cloud sur AWS avec Terraform. Vous verrez comment créer un VPC, des instances EC2 et des buckets S3 en utilisant les variables et bonnes pratiques que nous venons d'apprendre. Restez connectés !

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