Aller au contenu

Module 8 — Variables, types avancés et garde-fous

Durée indicative du cours : 1h45
Durée indicative de l’atelier : — (atelier naming / validations à venir)
Durée totale (cours + atelier) : ~2h30–3h00

Objectif du module

Le module 4 Basics a posé le catalogue des types et un aperçu de validation / sensitive.
Ici, on conçoit des interfaces (racine + modules) : compositions de types, optional(), validations robustes, attributs nullable / ephemeral / sensitive, et références built-in (path.*, terraform.workspace).

Rappel express des types

Type Usage typique
string / number / bool Scalaires
list(T) Suite ordonnée d’éléments homogènes
set(T) Ensemble unique, sans ordre
map(T) Dictionnaire clé → valeur homogène (ex. tags map(string))
object({…}) Structure hétérogène nommée
tuple([…]) Positions fixes typées (rare en interface publique)
any Échappatoire — à éviter en interface de module

Quand choisir quoi

Besoin Préférer
Plusieurs éléments homogènes ordonnés list
Ensemble unique sans ordre set
Tags / dictionnaire homogène map
Config structurée (name + cidr + …) object
Positions fixes typées tuple (documenter pourquoi)
« On verra plus tard » Pas any en API publique

Contraste déjà vu en Basics : list vs object.

optional() dans les objets

variable "subnet" {
  type = object({
    name       = string
    cidr       = string
    delegation = optional(string)           # omis → null
    nsg_id     = optional(string, null)     # défaut explicite
  })
}

Les champs optionnels assouplissent les tfvars et les appels module sans abandonner le typage.

Compositions fréquentes

variable "subnets" {
  type = list(object({
    name = string
    cidr = string
  }))
}

# Variante souvent meilleure pour for_each (clés = noms) :
variable "subnets_map" {
  type = map(object({
    cidr = string
  }))
}

Lien module 10

for_each aime les clés stables.
Une map(object(…)) est souvent plus sûre qu’une list + count.

Naming convention + validations

Construire la convention

Exemple d’équipe lab (à adapter) :

Ressource Motif
Resource group rg-<projet>-<env>-<alias>
VNet vnet-<projet>-<env>-<alias>
Subnet snet-<projet>-<env>-<alias>-<rôle>

Puis on encode avec locals + format / interpolation, et on valide tôt au plan.

locals {
  rg_name = "rg-${var.project}-${var.environment}-${var.alias}"
}

variable "alias" {
  type        = string
  description = "Suffixe court minuscules/chiffres"

  validation {
    condition     = length(var.alias) >= 2 && can(regex("^[a-z0-9]+$", var.alias))
    error_message = "alias : minuscules/chiffres, au moins 2 caractères."
  }
}

Rappel nommage Basics : Nommage et tags.

Opérateurs dans condition

La condition d’un bloc validation est une expression booléenne.

Opérateurs Rôle Exemple
== != Égalité / différence var.env == "lab"
> >= < <= Comparaison length(var.prefix) >= 3
&& \|\| Et / Ou var.env == "lab" && length(var.alias) > 0
! Négation !contains(local.forbidden, var.sku)
variable "node_count" {
  type = number

  validation {
    condition     = var.node_count >= 1 && var.node_count <= 5
    error_message = "node_count doit être entre 1 et 5."
  }
}

Fonctions fréquentes dans condition

Expression Rôle
contains(list, value) Valeur dans une liste autorisée
can(expr) true si expr réussit (absorbe l’erreur)
can(tonumber(…)) La chaîne est-elle numérique ?
can(regex(pattern, str)) Motif OK ?
alltrue(list(bool)) Toutes les conditions sont true
anytrue(list(bool)) Au moins une est true
variable "location" {
  type = string

  validation {
    condition     = contains(["switzerlandnorth", "switzerlandwest"], var.location)
    error_message = "location non autorisée pour ce lab."
  }
}

variable "extra_cidrs" {
  type = list(string)

  validation {
    condition = alltrue([
      for c in var.extra_cidrs : can(cidrhost(c, 0))
    ])
    error_message = "chaque entrée de extra_cidrs doit être un CIDR valide."
  }
}

can vs erreur brute

Sans can, un tonumber("abc") ou un mauvais regex peut faire échouer autrement.
Avec can, vous obtenez un false propre pour la validation.

Règles :

  • condition doit valoir true pour accepter la valeur.
  • Messages actionnables (quoi corriger).
  • Plusieurs blocs validation possibles sur la même variable.

Lab proposé — regex + naming

  1. Écrire la convention RG / VNet / subnet.
  2. Construire les noms en locals.
  3. Valider avec can(regex(…)).
  4. Tester dans terraform console.
  5. Cas négatifs (majuscules, underscore) → plan échoue.
  6. Cas positifs → déployer un lab léger.

Arguments avancés des variable

Attribut Basics Associate
type / default / description Oui Rappel
sensitive Aperçu Profondeur (state !)
validation Aperçu Pratique
nullable Non Oui
ephemeral Non Oui (selon version lab)

nullable

variable "override_name" {
  type     = string
  nullable = false   # null interdit ; omis ≠ null selon défaut
  default  = "lab"
}

Utile sur les interfaces de modules : clarifier si null est une valeur métier acceptée.

sensitive — masque ≠ protection du state

variable "api_token" {
  type      = string
  sensitive = true
}

sensitive masque beaucoup d’affichages CLI / UI.
Le secret peut quand même être dans le tfstate (souvent en clair) dès qu’une ressource ou un output le référence.

Démo formateur (à faire)

apply avec un faux secret → terraform output masqué → ouvrir / grepper le state → valeur visible.
Conclusion : ACL + chiffrement du backend, pas de commit du state, alternatives Key Vault / CI / ephemeral.

ephemeral et arguments write-only

Objectif examen TA-004 (4h) : distinguer sensitive (masque l’affichage, mais le state peut encore contenir la valeur) et les mécanismes éphémères (Terraform 1.10+ / famille 1.12 pour l’examen), qui visent à ne pas persister certains secrets dans le state ni dans les fichiers de plan.

Variable ephemeral

Une variable marquée ephemeral = true ne peut être utilisée que dans des contextes éphémères (par exemple un autre argument write-only, ou un bloc ephemeral). Elle n’est pas destinée à être stockée comme une variable classique dans le state.

variable "db_password" {
  type      = string
  ephemeral = true
  sensitive = true
}

Bloc ephemeral (ressource temporaire)

Le bloc ephemeral décrit une ressource temporaire pour l’opération en cours. Terraform ne la met pas dans le state comme une ressource managée classique.

Exemple conceptuel (provider random) :

ephemeral "random_password" "db" {
  length           = 20
  special          = true
  override_special = "!#$%-_"
}

La valeur se lit ensuite via ephemeral.random_password.db.result pour alimenter un argument write-only d’une ressource managée.

Arguments write-only (*_wo)

Certains providers exposent des arguments write-only (souvent suffixés _wo, avec une version _wo_version pour forcer une mise à jour). Terraform envoie la valeur au provider pendant l’opération, puis ne la conserve pas dans le state.

Schéma mental (lab) :

  1. Générer ou lire un secret en ephemeral.
  2. Le passer à un argument write-only (secret Azure Key Vault, mot de passe, etc. selon le provider).
  3. Vérifier que le state ne contient pas ce secret en clair (contrairement à un sensitive classique).

Documentation : Ephemeral values · Write-only arguments.

Message examen TA-004

sensitive = masquer (CLI / UI), pas « effacer du state ».
ephemeral / write-only = ne pas persister le secret dans state / plan.
Vault (ou Key Vault) reste une bonne pratique d’équipe pour stocker et pivoter les secrets hors Git.

Lab formateur

Sur le Terraform du lab (viser 1.12 comme l’examen 004), montrer un secret sensitive encore visible dans le state, puis un chemin ephemeral / write-only si le provider Azure du lab le permet. Sinon, démo courte avec random + un provider qui expose *_wo.

Références built-in — path.* et workspace

Ce ne sont pas des locals que vous écrivez : Terraform les fournit.

Concept Exemple Rôle
locals local.rg_name Valeur calculée par l’auteur
var. var.prefix Entrée externe
path.module chemin du module courant Fichiers relatifs au module
path.root module racine Ancrage depuis un sous-module
path.cwd répertoire de travail au plan Prudence en CI
terraform.workspace nom du workspace actif Logique légère par workspace
terraform.appversion version du binaire Diagnostics / conditions rares
output "paths" {
  description = "Comparer path.module, path.root et path.cwd"
  value = {
    module = path.module
    root   = path.root
    cwd    = path.cwd
  }
}

Démo sans cloud

À la racine, module et root sont souvent proches.
Dans un sous-module, l’écart path.module vs path.root devient visible.
Preferez path.module / path.root pour file / templatefile, pas path.cwd.

terraform.workspace

Voir aussi module 5 — workspaces CLI.

locals {
  name_suffix = terraform.workspace == "default" ? "lab" : terraform.workspace
  # ou : lookup({ default = "lab", dev = "dev", prod = "prod" }, terraform.workspace, "lab")
}

Limites

Utile en lab / démo.
Ne pas en faire la seule stratégie multi-env.
Distinguer workspaces CLI et workspaces Terraform Cloud (module 11).

Suite

Poursuivre avec le module 9 — Outputs et preconditions.
Pratique : interface de module typée + validations naming ; démo paths et workspace.