05 — Terraform : fondations#

Terraform, développé par HashiCorp, est l’outil de référence pour l”Infrastructure as Code déclarative. Il permet de décrire l’état cible d’une infrastructure dans des fichiers texte, puis de converger vers cet état de manière reproductible et auditée. Ce chapitre couvre les fondations du langage HCL, le cycle de vie des ressources, la gestion du state et les workspaces.

HCL — HashiCorp Configuration Language#

HCL est un langage déclaratif conçu pour être lisible par les humains et analysable par les machines. Il exprime la configuration sous forme de blocs imbriqués avec des attributs typés.

Structure syntaxique#

# Bloc de type "resource" avec deux étiquettes : type et nom local
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"   # attribut string
  instance_type = "t3.micro"

  # Bloc imbriqué
  tags = {
    Name        = "web-server"
    Environment = var.environment            # référence à une variable
  }
}

Types primitifs HCL :

  • string : "valeur"

  • number : 42, 3.14

  • bool : true, false

Types complexes :

  • list(type) : ["a", "b", "c"]

  • map(type) : { key = "value" }

  • set(type) : ensemble non ordonné sans doublons

  • object({...}) : structure typée

  • tuple([...]) : liste de types hétérogènes

Expressions :

# Interpolation
name = "server-${var.environment}-${count.index}"

# Opérateur ternaire
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

# For expression
subnet_ids = [for s in var.subnets : s.id if s.public]

# Splat expression
all_arns = aws_iam_role.roles[*].arn

Providers#

Les providers sont des plugins qui exposent les ressources d’une API (AWS, GCP, Azure, GitHub, Datadog, etc.). Ils doivent être déclarés et versionnés explicitement.

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"       # compatible 5.x, pas 6.x
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.5"
    }
  }
}

# provider.tf
provider "aws" {
  region  = var.aws_region
  profile = var.aws_profile

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

# Provider alternatif (multi-région)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

Resources et cycle de vie#

Une ressource représente un objet d’infrastructure géré par Terraform. Le cycle de vie standard comporte trois opérations : create, update (in-place ou recreate), destroy.

# main.tf — ressource EC2 avec lifecycle avancé
resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.private.id

  user_data = templatefile("${path.module}/scripts/init.sh", {
    db_host = aws_db_instance.main.endpoint
    app_env = var.environment
  })

  lifecycle {
    # Ne pas détruire avant de recréer (évite la coupure de service)
    create_before_destroy = true

    # Ignorer les changements d'AMI après déploiement initial
    ignore_changes = [ami, user_data]

    # Protection contre la suppression accidentelle
    prevent_destroy = true
  }
}

Comportement selon le type de changement :

  • Modification d’un attribut non-ForceNew → update in-place

  • Modification d’un attribut ForceNew → destroy + create (ou create_before_destroy)

  • terraform taint <resource> → force la recréation au prochain apply

Data sources#

Les data sources lisent des informations depuis l’infrastructure existante sans la gérer. Ils permettent de référencer des ressources créées en dehors de Terraform.

# Récupérer la dernière AMI Ubuntu 22.04
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Récupérer les AZs disponibles dans la région
data "aws_availability_zones" "available" {
  state = "available"
}

# Lire le state d'un autre projet Terraform
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-states"
    key    = "network/terraform.tfstate"
    region = "eu-west-1"
  }
}

Variables#

Les variables paramètrent les modules et rendent la configuration réutilisable.

# variables.tf
variable "environment" {
  description = "Environnement de déploiement"
  type        = string
  default     = "staging"

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

variable "db_password" {
  description = "Mot de passe de la base de données"
  type        = string
  sensitive   = true   # masqué dans les logs et le plan
  # pas de default : sera demandé interactivement ou via TF_VAR_db_password
}

variable "instance_config" {
  description = "Configuration de l'instance"
  type = object({
    instance_type = string
    disk_size_gb  = number
    tags          = map(string)
  })
  default = {
    instance_type = "t3.micro"
    disk_size_gb  = 20
    tags          = {}
  }
}

Ordre de priorité des valeurs (du plus fort au plus faible) :

  1. -var ou -var-file sur la ligne de commande

  2. Fichier terraform.tfvars ou *.auto.tfvars

  3. Variables d’environnement TF_VAR_<nom>

  4. Valeur default dans la déclaration

Outputs#

Les outputs exposent des valeurs calculées après l’apply, utilisables par d’autres modules ou comme résultat final.

# outputs.tf
output "instance_public_ip" {
  description = "IP publique de l'instance"
  value       = aws_instance.app.public_ip
}

output "db_endpoint" {
  description = "Endpoint de la base de données"
  value       = aws_db_instance.main.endpoint
  sensitive   = true   # ne pas afficher en clair
}

output "alb_dns_name" {
  description = "DNS du load balancer"
  value       = aws_lb.main.dns_name

  # Dépendance explicite (rarement nécessaire, Terraform les détecte)
  depends_on = [aws_lb_listener.http]
}

Locals#

Les locals permettent de définir des expressions réutilisables à l’intérieur d’un module, sans les exposer comme variables d’entrée.

locals {
  # Préfixe commun pour toutes les ressources
  name_prefix = "${var.project}-${var.environment}"

  # Tags enrichis
  common_tags = merge(var.extra_tags, {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
    UpdatedAt   = timestamp()
  })

  # Calcul conditionnel
  is_production = var.environment == "prod"
  replica_count = local.is_production ? 3 : 1
}

resource "aws_db_instance" "main" {
  identifier     = "${local.name_prefix}-db"
  multi_az       = local.is_production
  tags           = local.common_tags
}

Cycle plan / apply / destroy#

# Initialiser le répertoire (télécharge providers et modules)
terraform init

# Vérifier la syntaxe et la cohérence
terraform validate

# Formater le code selon les conventions HCL
terraform fmt -recursive

# Générer et afficher le plan de changements
terraform plan -out=tfplan

# Appliquer le plan sauvegardé
terraform apply tfplan

# Appliquer directement (demande confirmation)
terraform apply -var-file=prod.tfvars

# Détruire l'infrastructure (demande confirmation)
terraform destroy

# Inspecter les ressources gérées
terraform show
terraform state list
terraform output -json

State Terraform#

Le state est le registre central qui fait le lien entre la configuration HCL et les objets réels de l’infrastructure. Sans state, Terraform ne saurait pas quelles ressources il gère.

State local vs backend distant#

# backend.tf — Backend S3 avec verrouillage DynamoDB
terraform {
  backend "s3" {
    bucket         = "my-terraform-states-prod"
    key            = "services/api/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:eu-west-1:123456789:key/abc-def"

    # Verrouillage pour éviter les apply concurrents
    dynamodb_table = "terraform-state-locks"
  }
}

# Backend GCS (Google Cloud Storage)
terraform {
  backend "gcs" {
    bucket = "my-terraform-states"
    prefix = "services/api"
  }
}

Opérations sur le state :

# Lister les ressources dans le state
terraform state list

# Afficher les détails d'une ressource
terraform state show aws_instance.app

# Déplacer une ressource dans le state (refactoring)
terraform state mv aws_instance.app aws_instance.web

# Retirer une ressource du state sans la détruire
terraform state rm aws_instance.legacy

# Rafraîchir le state depuis l'infrastructure réelle
terraform refresh

Workspaces#

Les workspaces permettent de gérer plusieurs états indépendants depuis la même configuration.

# Créer et utiliser un workspace
terraform workspace new staging
terraform workspace new prod
terraform workspace list
terraform workspace select prod

# Dans la configuration, accéder au workspace courant
locals {
  env_config = {
    dev     = { instance_type = "t3.micro",  replica_count = 1 }
    staging = { instance_type = "t3.small",  replica_count = 2 }
    prod    = { instance_type = "t3.medium", replica_count = 3 }
  }

  current_config = local.env_config[terraform.workspace]
}

Limites des workspaces

Les workspaces partagent la même configuration et le même backend. Pour une isolation forte (comptes AWS séparés, périmètres de sécurité distincts), préférez des répertoires Terraform indépendants ou Terragrunt.

Immutabilité et configuration drift#

L’IaC déclarative repose sur le principe d”immutabilité : plutôt que de patcher une ressource existante, on la recrée depuis un état propre. Cela garantit la reproductibilité mais exige une discipline rigoureuse.

Le configuration drift désigne le décalage entre l’état décrit dans le code et l’état réel de l’infrastructure, causé par des modifications manuelles hors Terraform.

# Détecter le drift : terraform plan rafraîchit le state et compare
terraform plan -refresh-only

# Forcer le rafraîchissement
terraform refresh

Bonne pratique

Activez des alertes sur les modifications manuelles (AWS Config, GCP Policy, Azure Policy) et considérez le plan -refresh-only comme une étape de votre pipeline CI pour détecter les drifts tôt.


Visualisations#

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.patheffects as pe
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Visualisation 1 : Cycle de vie d'une ressource Terraform

fig, ax = plt.subplots(figsize=(13, 5))
ax.set_xlim(0, 14)
ax.set_ylim(-1, 3)
ax.axis("off")
ax.set_title("Cycle de vie d'une ressource Terraform", fontsize=14, fontweight="bold", pad=18)

# États et positions
etats = [
    ("Non géré",   1.0,  1.5, "#adb5bd"),
    ("Planifié",   3.2,  1.5, "#74c0fc"),
    ("Créé",       5.4,  1.5, "#51cf66"),
    ("Modifié",    7.6,  1.5, "#ffd43b"),
    ("Tainted",    9.8,  1.5, "#ff922b"),
    ("Détruit",   12.0,  1.5, "#ff6b6b"),
]

for label, x, y, color in etats:
    box = FancyBboxPatch((x - 0.9, y - 0.45), 1.8, 0.9,
                         boxstyle="round,pad=0.08",
                         facecolor=color, edgecolor="white", linewidth=2, zorder=3)
    ax.add_patch(box)
    ax.text(x, y, label, ha="center", va="center", fontsize=9.5, fontweight="bold", color="white" if color not in ["#ffd43b", "#adb5bd"] else "#333")

# Flèches et labels de transitions
transitions = [
    (1.9, 3.1, "terraform init\n+ plan"),
    (4.1, 5.3, "apply"),
    (6.3, 7.5, "apply\n(changement)"),
    (8.5, 9.7, "terraform taint\nou drift"),
    (10.7, 11.9, "apply\n(recreate)"),
]

for x_start, x_end, label in transitions:
    ax.annotate("", xy=(x_end - 0.9, 1.5), xytext=(x_start + 0.9, 1.5),
                arrowprops=dict(arrowstyle="->", color="#495057", lw=2.0))
    mid = (x_start + x_end) / 2
    ax.text(mid, 2.05, label, ha="center", va="bottom", fontsize=8, color="#495057", style="italic")

# Arc retour destroy -> non géré
ax.annotate("", xy=(2.0, 1.05), xytext=(11.1, 1.05),
            arrowprops=dict(arrowstyle="->", color="#868e96", lw=1.5,
                            connectionstyle="arc3,rad=0.4"))
ax.text(6.5, 0.05, "terraform destroy", ha="center", fontsize=8, color="#868e96", style="italic")

plt.savefig("lifecycle_terraform.png", dpi=120, bbox_inches="tight")
plt.show()
_images/9b4b7cd77155b9e2de3294df638c0145a8b0c19b3844b6ea58ebefad034d3bb2.png
# Visualisation 2 : Configuration drift — état désiré vs état réel

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

np.random.seed(42)
t = np.linspace(0, 10, 200)

# État désiré : stable par paliers
desired = np.where(t < 4, 3, np.where(t < 7, 5, 5))

# État réel : suit le désiré mais dérive progressivement entre les applies
drift = desired.copy().astype(float)
# Drift entre t=2 et t=4
mask1 = (t >= 2) & (t < 4)
drift[mask1] += 0.6 * (t[mask1] - 2)
# Drift entre t=5 et t=7
mask2 = (t >= 5) & (t < 7)
drift[mask2] -= 0.5 * (t[mask2] - 5)
# Ajout de bruit
drift += np.random.normal(0, 0.08, len(t))

fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(t, desired, color="#4dabf7", linewidth=2.5, linestyle="--", label="État désiré (HCL)")
ax.plot(t, drift,   color="#ff6b6b", linewidth=2,   linestyle="-",  label="État réel (infrastructure)")

# Zone de drift
ax.fill_between(t, desired, drift, where=np.abs(desired - drift) > 0.05,
                alpha=0.18, color="#ff6b6b", label="Zone de drift")

# Marqueurs d'apply
apply_times = [0.1, 4.0, 7.0]
for ta in apply_times:
    idx = np.argmin(np.abs(t - ta))
    ax.axvline(x=ta, color="#51cf66", linewidth=1.5, linestyle=":", alpha=0.8)
    ax.text(ta + 0.1, 5.7, "terraform\napply", fontsize=7.5, color="#2f9e44", va="top")

ax.set_xlabel("Temps (semaines)")
ax.set_ylabel("Nombre d'instances actives")
ax.set_title("Configuration drift : état désiré vs état réel", fontsize=13, fontweight="bold")
ax.legend(loc="lower right")
ax.set_ylim(1.5, 6.5)

plt.savefig("terraform_drift.png", dpi=120, bbox_inches="tight")
plt.show()
_images/09c4f774a8f9389c376dbb6b8fd7657d357dd2db748aa0ee7445bfed9adbd8f9.png
# Visualisation 3 : Risques comparés selon la stratégie de gestion du state

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

strategies = ["State local\n(fichier .tfstate)", "Backend distant\n(S3 + DynamoDB)", "Terraform Cloud\n/ HCP Terraform"]
categories = [
    "Perte accidentelle",
    "Apply concurrent",
    "Accès non autorisé",
    "Audit / historique",
    "Complexité setup",
]

# Score de risque (0 = risque faible, 10 = risque élevé)
scores = np.array([
    [9, 2, 1],   # Perte accidentelle
    [8, 1, 1],   # Apply concurrent
    [7, 3, 2],   # Accès non autorisé
    [8, 3, 1],   # Audit / historique
    [1, 5, 2],   # Complexité setup
])

x = np.arange(len(categories))
width = 0.22
colors = ["#ff6b6b", "#ffd43b", "#51cf66"]

fig, ax = plt.subplots(figsize=(12, 6))
for i, (strat, color) in enumerate(zip(strategies, colors)):
    bars = ax.bar(x + i * width, scores[:, i], width, label=strat,
                  color=color, alpha=0.85, edgecolor="white", linewidth=1.2)
    for bar in bars:
        h = bar.get_height()
        ax.text(bar.get_x() + bar.get_width() / 2, h + 0.15, str(int(h)),
                ha="center", va="bottom", fontsize=8, fontweight="bold")

ax.set_xticks(x + width)
ax.set_xticklabels(categories, fontsize=10)
ax.set_ylabel("Score de risque (0 = faible, 10 = élevé)")
ax.set_title("Risques liés à la gestion du state Terraform\nselon la stratégie de backend", fontsize=13, fontweight="bold")
ax.set_ylim(0, 11.5)
ax.legend(title="Stratégie", loc="upper right")

plt.savefig("terraform_state_risks.png", dpi=120, bbox_inches="tight")
plt.show()
_images/cc0f7d8974633166f38f42e329e6f1083ff0929ea90a2234281e47d5d654c958.png

Résumé#

  1. HCL est un langage déclaratif typé : blocs, attributs, expressions for, opérateurs ternaires et splat couvrent la majorité des besoins de configuration.

  2. Les providers versionnés dans required_providers garantissent la reproductibilité des déploiements et évitent les breaking changes involontaires.

  3. Le cycle de vie d’une ressource (create_before_destroy, ignore_changes, prevent_destroy) permet de contrôler finement le comportement de Terraform lors des mises à jour.

  4. Les data sources permettent de lire l’infrastructure existante sans la gérer, créant un pont entre Terraform et les ressources provisionnées hors-bande.

  5. La combinaison variables / outputs / locals structure la configuration : les variables paramètrent, les locals calculent, les outputs exposent.

  6. Le state est le registre central de Terraform ; son placement sur un backend distant (S3+DynamoDB, GCS, Terraform Cloud) est non négociable en équipe pour éviter les conflits et les pertes.

  7. Le configuration drift est détectable via terraform plan -refresh-only ; le combattre exige de bannir les modifications manuelles et d’automatiser les applies.

  8. Les workspaces offrent une isolation légère par environnement mais ne remplacent pas une séparation de comptes cloud pour les environnements de production critiques.

  9. Le cycle fmt validate plan apply doit être intégré dans le pipeline CI pour garantir la qualité et la traçabilité de chaque changement d’infrastructure.

  10. L”immutabilité — recréer plutôt que patcher — est le principe fondateur qui rend l’IaC Terraform prévisible et reproductible.