Aller au contenu principal
Intermédiaire 6 chapitres

đŸ—ïž Terraform — Infrastructure as Code

Progression 6 chapitres

Programme

L’Infrastructure as Code en 2026 : pourquoi tu ne peux plus t’en passer

On est en 2026. Si tu gĂšres encore des serveurs en cliquant dans une console web, tu vis dangereusement. Chaque clic non documentĂ©, c’est une bombe Ă  retardement : un collĂšgue qui ne sait pas reproduire ton setup, un disaster recovery qui prend des jours au lieu de minutes, une infrastructure qui dĂ©rive silencieusement de ce qu’elle devrait ĂȘtre.

L’Infrastructure as Code (IaC), c’est la rĂ©ponse dĂ©finitive Ă  ce chaos. Tu dĂ©cris ton infrastructure dans des fichiers texte, versionnĂ©s dans Git, revus en pull request, appliquĂ©s automatiquement. Ton infrastructure devient aussi fiable et reproductible que ton code applicatif.

Et dans l’écosystĂšme IaC, Terraform reste le roi. Pas par inertie — par mĂ©rite. Son approche dĂ©clarative, son Ă©cosystĂšme de providers gigantesque (4 000+), sa communautĂ© massive et son langage HCL lisible en font l’outil de rĂ©fĂ©rence pour gĂ©rer du cloud, du on-premise, du SaaS, et tout ce qui a une API.

Que tu déploies sur Hetzner, Infomaniak, AWS, GCP ou Azure, Terraform te donne un workflow unifié : init, plan, apply, destroy. Quatre commandes pour gouverner toute ton infrastructure.

Cette formation te prend par la main depuis le premier terraform init jusqu’à un pipeline CI/CD complet avec validation de policies. Pas de thĂ©orie creuse — du code, des patterns concrets, et des architectures que tu peux mettre en production demain.


OpenTofu vs Terraform : le fork qu’il faut comprendre

En août 2023, HashiCorp a changé la licence de Terraform : passage de MPL 2.0 (open source) à BSL 1.1 (Business Source License). La communauté a réagi en créant OpenTofu, un fork maintenu par la Linux Foundation.

Ce que ça change concrÚtement

AspectTerraformOpenTofu
LicenceBSL 1.1 (non open source)MPL 2.0 (open source)
MainteneurHashiCorp (IBM)Linux Foundation
CLIterraformtofu
Registryregistry.terraform.ioregistry.opentofu.org
Compatibilité HCLRéférenceCompatible ~99%
State encryptionNon natifNatif depuis 1.7
Features exclusivesMoved blocks, checksEarly variable eval, for_each sur providers

Faut-il migrer vers OpenTofu ?

La réponse pragmatique : ça dépend de ton contexte.

  • Startup / projet perso → OpenTofu est un choix solide et libre
  • Entreprise avec support HashiCorp → Terraform reste cohĂ©rent avec ton contrat
  • Nouveau projet → Évalue les deux, le coĂ»t de migration est quasi nul

La bonne nouvelle : tout ce que tu apprends dans cette formation s’applique aux deux. Le langage HCL est identique, les concepts sont les mĂȘmes, les providers sont compatibles. Quand on Ă©crit terraform dans les exemples, tu peux mentalement lire tofu si tu prĂ©fĂšres.

# Les commandes sont interchangeables
terraform init    # ou: tofu init
terraform plan    # ou: tofu plan
terraform apply   # ou: tofu apply
terraform destroy # ou: tofu destroy

Parcours de formation

Cette formation est structurée en 6 modules progressifs. Les trois premiers correspondent à des chapitres de cours détaillés déjà disponibles :

Chapitres de cours disponibles :

  1. 📘 Chapitre 1 — Fondamentaux de Terraform : Installation, premiers providers, workflow de base
  2. 📗 Chapitre 2 — Variables et expressions HCL : Variables, outputs, boucles, conditions
  3. 📕 Chapitre 3 — State et Modules : State remote, modules, workspaces

Chaque module ci-dessous te donne un aperçu riche du contenu, avec des extraits de code concrets. Les chapitres de cours vont encore plus en profondeur avec des exercices pratiques.

Vue d’ensemble du parcours

Module 1 : Concepts & Installation
    └── Comprendre l'IaC, installer Terraform, premier apply
         │
Module 2 : Langage HCL en profondeur
    └── Resources, data sources, variables, boucles, conditions
         │
Module 3 : State Management
    └── Remote state, locking, workspaces, import
         │
Module 4 : Modules réutilisables
    └── CrĂ©er, publier, versionner des modules
         │
Module 5 : Production & CI/CD
    └── Pipelines GitHub Actions, policies OPA/Sentinel

Module 1 — Concepts & Installation

📘 Chapitre de cours : Fondamentaux de Terraform

Pourquoi Terraform plutît qu’un autre outil ?

Avant de plonger dans le code, il faut comprendre pourquoi Terraform a gagné la bataille des outils IaC. La réponse tient en trois mots : déclaratif, agnostique, état.

Tu ne dis pas Ă  Terraform comment faire les choses (impĂ©ratif). Tu lui dis ce que tu veux (dĂ©claratif), et il calcule le chemin pour y arriver. Tu veux 3 serveurs avec un load balancer ? Tu le dĂ©clares. Terraform compare l’état actuel avec l’état dĂ©sirĂ© et applique uniquement les changements nĂ©cessaires.

Contrairement Ă  CloudFormation (AWS only) ou ARM Templates (Azure only), Terraform est cloud-agnostic. Un mĂȘme workflow, un mĂȘme langage, pour tous tes providers. Tu peux gĂ©rer ton infra Hetzner, tes DNS Cloudflare, tes secrets Vault et tes repos GitHub — tout dans le mĂȘme projet.

Et le state (fichier d’état) est le secret de sa puissance. Terraform maintient une cartographie complĂšte de ton infrastructure rĂ©elle. Il sait ce qui existe, ce qui a changĂ©, ce qu’il faut crĂ©er ou dĂ©truire. C’est ce qui rend les plan si prĂ©cis.

Installation multi-plateforme

# macOS avec Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Linux (Ubuntu/Debian)
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# Avec asdf (recommandé pour gérer plusieurs versions)
asdf plugin add terraform
asdf install terraform 1.9.8
asdf global terraform 1.9.8

# OpenTofu alternative
brew install opentofu          # macOS
snap install --classic opentofu # Linux

# Vérification
terraform version
# Terraform v1.9.8

Premier projet : un serveur Hetzner

Hetzner est parfait pour apprendre : serveurs pas chers, API propre, provider Terraform mature. Voici ton premier projet complet :

# versions.tf — Fixer les versions pour la reproductibilitĂ©
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.49"
    }
  }
}

# provider.tf — Configuration du provider Hetzner
provider "hcloud" {
  token = var.hcloud_token
}

# variables.tf — Variables d'entrĂ©e
variable "hcloud_token" {
  description = "Token API Hetzner Cloud"
  type        = string
  sensitive   = true
}

variable "server_name" {
  description = "Nom du serveur"
  type        = string
  default     = "web-01"
}

variable "server_type" {
  description = "Type de serveur Hetzner"
  type        = string
  default     = "cx22"   # 2 vCPU, 4 GB RAM
}

variable "location" {
  description = "Datacenter Hetzner"
  type        = string
  default     = "fsn1"   # Falkenstein, Allemagne
}
# main.tf — Ressources principales
resource "hcloud_ssh_key" "default" {
  name       = "my-ssh-key"
  public_key = file("~/.ssh/id_ed25519.pub")
}

resource "hcloud_server" "web" {
  name        = var.server_name
  server_type = var.server_type
  location    = var.location
  image       = "ubuntu-24.04"
  ssh_keys    = [hcloud_ssh_key.default.id]

  labels = {
    environment = "dev"
    managed_by  = "terraform"
  }

  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable --now nginx
    echo "<h1>DĂ©ployĂ© par Terraform đŸ—ïž</h1>" > /var/www/html/index.html
  EOF
}

# outputs.tf — Valeurs de sortie
output "server_ip" {
  description = "Adresse IP publique du serveur"
  value       = hcloud_server.web.ipv4_address
}

output "server_status" {
  description = "Statut du serveur"
  value       = hcloud_server.web.status
}

Le workflow fondamental

# 1. Initialiser — tĂ©lĂ©charge les providers
terraform init

# 2. Valider — vĂ©rifie la syntaxe
terraform validate

# 3. Planifier — preview des changements (TOUJOURS avant apply)
terraform plan -out=tfplan

# 4. Appliquer — crĂ©e l'infrastructure
terraform apply tfplan

# 5. Inspecter — voir l'Ă©tat actuel
terraform show
terraform output server_ip

# 6. DĂ©truire — supprime tout (attention !)
terraform destroy

Multi-cloud dÚs le départ

Terraform brille quand tu gĂšres plusieurs providers. Voici un exemple combinant Hetzner et Cloudflare :

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.49"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.48"
    }
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

# Serveur Hetzner
resource "hcloud_server" "app" {
  name        = "app-prod"
  server_type = "cx32"
  location    = "fsn1"
  image       = "ubuntu-24.04"
  ssh_keys    = [hcloud_ssh_key.default.id]
}

# DNS Cloudflare pointant vers le serveur
resource "cloudflare_record" "app" {
  zone_id = var.cloudflare_zone_id
  name    = "app"
  content = hcloud_server.app.ipv4_address
  type    = "A"
  ttl     = 300
  proxied = true
}

Ce que tu vas apprendre dans ce module

  • ✅ Les concepts fondamentaux de l’IaC et pourquoi c’est non-nĂ©gociable en 2026
  • ✅ Installer Terraform (ou OpenTofu) sur n’importe quelle plateforme
  • ✅ Configurer des providers (Hetzner, Infomaniak, AWS, GCP, Cloudflare)
  • ✅ Écrire ton premier fichier HCL et dĂ©ployer une vraie ressource
  • ✅ MaĂźtriser le workflow init → validate → plan → apply → destroy
  • ✅ Comprendre le fichier de state et pourquoi il est critique
  • ✅ Organiser un projet Terraform proprement (convention de fichiers)

Module 2 — Langage HCL en profondeur

📗 Chapitre de cours : Variables et expressions HCL

HCL : un langage pensĂ© pour l’infrastructure

HCL (HashiCorp Configuration Language) n’est ni un langage de programmation classique, ni du simple YAML. C’est un langage dĂ©claratif avec des capacitĂ©s d’expression : tu dĂ©cris ce que tu veux, mais tu peux y injecter de la logique (boucles, conditions, transformations) quand c’est nĂ©cessaire.

La force de HCL, c’est sa lisibilitĂ©. Un fichier Terraform bien Ă©crit se lit presque comme de la documentation. MĂȘme quelqu’un qui ne connaĂźt pas Terraform peut comprendre ce que fait ton code. C’est un avantage Ă©norme pour la revue de code et l’onboarding de nouveaux membres d’équipe.

Mais ne te laisse pas tromper par cette simplicitĂ© apparente. HCL a des fonctionnalitĂ©s puissantes : types complexes (maps, objets, tuples), fonctions intĂ©grĂ©es (100+), expressions conditionnelles, boucles for_each et for, blocs dynamiques, et bien plus. MaĂźtriser ces outils, c’est la diffĂ©rence entre du Terraform basique et du Terraform Ă©lĂ©gant.

Resources et Data Sources

Les resources sont ce que Terraform crée et gÚre. Les data sources sont ce que Terraform lit sans modifier.

# Resource — Terraform gùre le cycle de vie complet
resource "hcloud_network" "main" {
  name     = "production-network"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "app" {
  network_id   = hcloud_network.main.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

# Data source — Lecture seule d'une ressource existante
data "hcloud_image" "ubuntu" {
  name              = "ubuntu-24.04"
  with_architecture = "x86"
  most_recent       = true
}

data "hcloud_ssh_keys" "all" {
  with_selector = "managed_by=terraform"
}

# Utilisation du data source dans une resource
resource "hcloud_server" "app" {
  name        = "app-server"
  server_type = "cx22"
  image       = data.hcloud_image.ubuntu.id
  ssh_keys    = data.hcloud_ssh_keys.all.ssh_keys[*].id
  location    = "fsn1"
}

Variables : types et validation

# String simple
variable "environment" {
  description = "Environnement de déploiement"
  type        = string
  default     = "dev"

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

# Nombre avec contrainte
variable "instance_count" {
  description = "Nombre de serveurs à créer"
  type        = number
  default     = 2

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

# Map typée
variable "server_config" {
  description = "Configuration des serveurs par environnement"
  type = map(object({
    server_type = string
    count       = number
    location    = string
    labels      = map(string)
  }))

  default = {
    dev = {
      server_type = "cx22"
      count       = 1
      location    = "fsn1"
      labels      = { env = "dev" }
    }
    prod = {
      server_type = "cx42"
      count       = 3
      location    = "nbg1"
      labels      = { env = "prod", critical = "true" }
    }
  }
}

# Liste d'objets
variable "firewall_rules" {
  description = "RĂšgles de firewall"
  type = list(object({
    direction  = string
    protocol   = string
    port       = string
    source_ips = list(string)
  }))

  default = [
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "22"
      source_ips = ["0.0.0.0/0", "::/0"]
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "443"
      source_ips = ["0.0.0.0/0", "::/0"]
    }
  ]
}

Boucles et itérations

# for_each avec une map — un serveur par environnement
resource "hcloud_server" "env" {
  for_each = var.server_config

  name        = "${each.key}-server"
  server_type = each.value.server_type
  location    = each.value.location
  image       = "ubuntu-24.04"
  labels      = each.value.labels
}

# count — plusieurs instances identiques
resource "hcloud_server" "worker" {
  count = var.instance_count

  name        = "worker-${format("%02d", count.index + 1)}"
  server_type = "cx22"
  location    = "fsn1"
  image       = "ubuntu-24.04"

  labels = {
    role  = "worker"
    index = tostring(count.index)
  }
}

# Expression for — transformer une liste
locals {
  # Créer une map d'IPs à partir des serveurs
  server_ips = {
    for name, server in hcloud_server.env :
    name => server.ipv4_address
  }

  # Filtrer une liste
  prod_servers = [
    for name, server in hcloud_server.env :
    server.ipv4_address
    if server.labels["env"] == "prod"
  ]

  # Flatten pour les structures imbriquées
  all_subnets = flatten([
    for env, config in var.environments : [
      for subnet in config.subnets : {
        env    = env
        cidr   = subnet.cidr
        name   = subnet.name
      }
    ]
  ])
}

Blocs dynamiques

# Firewall avec des rĂšgles dynamiques
resource "hcloud_firewall" "app" {
  name = "app-firewall"

  dynamic "rule" {
    for_each = var.firewall_rules
    content {
      direction  = rule.value.direction
      protocol   = rule.value.protocol
      port       = rule.value.port
      source_ips = rule.value.source_ips
    }
  }
}

Expressions conditionnelles

# Condition ternaire
resource "hcloud_server" "app" {
  name        = "app-${var.environment}"
  server_type = var.environment == "prod" ? "cx42" : "cx22"
  location    = var.environment == "prod" ? "nbg1" : "fsn1"
  image       = "ubuntu-24.04"
  backups     = var.environment == "prod" ? true : false
}

# Création conditionnelle avec count
resource "hcloud_floating_ip" "lb" {
  count = var.environment == "prod" ? 1 : 0

  type          = "ipv4"
  home_location = "nbg1"
  description   = "IP flottante pour le load balancer prod"
}

# Outputs conditionnels
output "floating_ip" {
  value = var.environment == "prod" ? hcloud_floating_ip.lb[0].ip_address : "N/A"
}

Fonctions utiles au quotidien

locals {
  # Fichiers et templates
  user_data  = file("${path.module}/scripts/init.sh")
  config     = templatefile("${path.module}/templates/nginx.conf.tpl", {
    server_name = var.domain
    upstream    = var.app_port
  })

  # Manipulation de strings
  name_upper  = upper(var.project_name)
  name_slug   = replace(lower(var.project_name), " ", "-")
  parts       = split("-", var.resource_name)

  # Manipulation de collections
  merged_tags = merge(var.default_tags, var.extra_tags)
  unique_ips  = distinct(var.allowed_ips)
  sorted      = sort(var.server_names)
  
  # Lookups
  ami_id      = lookup(var.ami_map, var.region, "ami-default")
  
  # Encodage
  config_json   = jsonencode(var.app_config)
  config_base64 = base64encode(local.config_json)

  # CIDR
  subnet_cidrs = [for i in range(4) : cidrsubnet("10.0.0.0/16", 8, i)]
  # Résultat : ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

Ce que tu vas apprendre dans ce module

  • ✅ La diffĂ©rence entre resources, data sources et locals
  • ✅ Le systĂšme de types HCL (string, number, bool, list, map, object, tuple)
  • ✅ Valider les entrĂ©es avec des blocs validation
  • ✅ ItĂ©rer avec count, for_each et les expressions for
  • ✅ CrĂ©er des blocs dynamiques pour les structures rĂ©pĂ©titives
  • ✅ Utiliser les conditions ternaires et la crĂ©ation conditionnelle
  • ✅ MaĂźtriser les fonctions built-in les plus utiles
  • ✅ Organiser ton code avec locals pour Ă©viter la duplication

Module 3 — State Management

📕 Chapitre de cours : State et Modules

Le state : le cƓur de Terraform

Le fichier state (terraform.tfstate) est la piĂšce maĂźtresse de Terraform. C’est une base de donnĂ©es JSON qui cartographie la correspondance entre tes ressources HCL et les ressources rĂ©elles dans le cloud. Sans lui, Terraform est aveugle.

Par dĂ©faut, le state est stockĂ© localement sur ta machine. C’est suffisant pour apprendre, mais en production, c’est un anti-pattern dangereux. Pourquoi ? Parce que le state contient des donnĂ©es sensibles (mots de passe, tokens), parce qu’il doit ĂȘtre partagĂ© entre les membres de l’équipe, et parce qu’il doit ĂȘtre protĂ©gĂ© contre les modifications concurrentes.

La solution : le remote state. Tu stockes ton state dans un backend distant (S3, GCS, Azure Blob, Terraform Cloud) avec du locking pour empĂȘcher deux personnes d’appliquer des changements en mĂȘme temps. C’est la premiĂšre chose Ă  configurer quand tu passes en production.

Ce module te montre comment configurer un backend robuste, gérer les workspaces pour séparer tes environnements, et maßtriser les commandes de state pour les situations délicates (import, move, remove).

Remote state avec S3

# backend.tf — Configuration du backend S3
terraform {
  backend "s3" {
    bucket         = "devopslab-terraform-state"
    key            = "prod/infrastructure/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
    
    # Optionnel : KMS pour le chiffrement
    kms_key_id     = "arn:aws:kms:eu-central-1:123456789:key/abc-def"
  }
}
# state-infra/main.tf — CrĂ©er le bucket et la table de lock
# (À appliquer une seule fois avec un state local)

provider "aws" {
  region = "eu-central-1"
}

resource "aws_s3_bucket" "terraform_state" {
  bucket = "devopslab-terraform-state"

  # EmpĂȘcher la suppression accidentelle
  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

State locking en action

# Le locking est automatique avec DynamoDB
# Quand quelqu'un lance un apply, les autres voient :

$ terraform apply
╷
│ Error: Error acquiring the state lock
│
│ Error message: ConditionalCheckFailedException: The conditional request failed
│ Lock Info:
│   ID:        a1b2c3d4-e5f6-7890-abcd-ef1234567890
│   Path:      devopslab-terraform-state/prod/terraform.tfstate
│   Operation: OperationTypeApply
│   Who:       dany@macbook
│   Version:   1.9.8
│   Created:   2026-03-20 08:15:32.123456 +0000 UTC
│
│ Terraform acquires a state lock to protect the state from being
│ written by multiple users at the same time.

# En cas de lock orphelin (crash), force unlock :
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890

Workspaces : un state par environnement

# Créer et utiliser des workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# Lister les workspaces
terraform workspace list
#   default
#   dev
# * staging
#   prod

# Changer de workspace
terraform workspace select prod
# Utiliser le workspace dans la configuration
locals {
  env_config = {
    dev = {
      server_type   = "cx22"
      instance_count = 1
      domain        = "dev.devopslab.ch"
    }
    staging = {
      server_type   = "cx32"
      instance_count = 2
      domain        = "staging.devopslab.ch"
    }
    prod = {
      server_type   = "cx42"
      instance_count = 3
      domain        = "devopslab.ch"
    }
  }

  current = local.env_config[terraform.workspace]
}

resource "hcloud_server" "app" {
  count = local.current.instance_count

  name        = "${terraform.workspace}-app-${count.index + 1}"
  server_type = local.current.server_type
  location    = "fsn1"
  image       = "ubuntu-24.04"

  labels = {
    environment = terraform.workspace
    managed_by  = "terraform"
  }
}

Import : intégrer des ressources existantes

# Importer une ressource existante dans le state
# Syntaxe : terraform import <adresse_resource> <id_cloud>

# Importer un serveur Hetzner existant
terraform import hcloud_server.legacy 12345678

# Importer un enregistrement DNS Cloudflare
terraform import cloudflare_record.existing zone-id/record-id

# Depuis Terraform 1.5+ : import par bloc (recommandé)
# Import par bloc — plus propre et reproductible
import {
  to = hcloud_server.legacy
  id = "12345678"
}

import {
  to = hcloud_network.main
  id = "98765"
}

# Générer le code HCL correspondant
# terraform plan -generate-config-out=generated.tf

Commandes de state avancées

# Lister toutes les ressources dans le state
terraform state list

# Voir le détail d'une ressource
terraform state show hcloud_server.app[0]

# Renommer une ressource (refactoring sans recréation)
terraform state mv hcloud_server.web hcloud_server.app

# Déplacer dans un module
terraform state mv hcloud_server.app module.compute.hcloud_server.app

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

# Taint — forcer la recrĂ©ation (dĂ©prĂ©ciĂ©, utilise -replace)
terraform apply -replace="hcloud_server.app[0]"

Ce que tu vas apprendre dans ce module

  • ✅ Pourquoi le state local est dangereux en production
  • ✅ Configurer un backend S3 avec chiffrement et locking DynamoDB
  • ✅ Migrer d’un state local vers un state remote
  • ✅ Utiliser les workspaces pour sĂ©parer dev/staging/prod
  • ✅ Importer des ressources existantes (CLI et blocs import)
  • ✅ Manipuler le state avec mv, rm, show et list
  • ✅ RĂ©cupĂ©rer d’un state corrompu ou d’un lock orphelin
  • ✅ Partager des donnĂ©es entre projets avec terraform_remote_state

Module 4 — Modules rĂ©utilisables

Des copier-coller aux modules

Si tu te retrouves Ă  copier-coller du HCL entre projets, c’est le signal : tu as besoin de modules. Un module Terraform, c’est simplement un rĂ©pertoire contenant des fichiers .tf que tu peux appeler avec des paramĂštres. C’est la fonction dans un langage de programmation — mĂȘme concept de rĂ©utilisabilitĂ© et d’abstraction.

Les bons modules transforment des dizaines de lignes de configuration rĂ©pĂ©titive en un appel de 5 lignes. Ils encapsulent les bonnes pratiques, imposent des conventions (naming, tagging, sĂ©curitĂ©) et rendent ton infrastructure composable. Au lieu de redĂ©finir un VPC complet Ă  chaque projet, tu appelles module "network" et c’est rĂ©glĂ©.

Mais attention : un mauvais module est pire que pas de module. Un module trop abstrait, avec 50 variables et 200 lignes de conditions, devient un cauchemar Ă  maintenir. La rĂšgle d’or : un module doit ĂȘtre opiniated. Il fait un truc, il le fait bien, et il impose les bonnes pratiques plutĂŽt que de tout rendre configurable.

Dans ce module, tu vas créer tes propres modules, comprendre la structure standard, le versioning, et comment les publier pour ton équipe ou la communauté.

Structure d’un module

modules/
└── hetzner-server/
    ├── main.tf          # Ressources principales
    ├── variables.tf     # EntrĂ©es du module
    ├── outputs.tf       # Sorties du module
    ├── versions.tf      # Contraintes de versions
    ├── README.md        # Documentation
    └── examples/
        └── basic/
            ├── main.tf
            └── terraform.tfvars

Créer un module complet

# modules/hetzner-server/versions.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = ">= 1.45.0"
    }
  }
}
# modules/hetzner-server/variables.tf
variable "name" {
  description = "Nom du serveur"
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,62}$", var.name))
    error_message = "Le nom doit ĂȘtre en lowercase, 3-63 caractĂšres, commencer par une lettre."
  }
}

variable "server_type" {
  description = "Type de serveur Hetzner"
  type        = string
  default     = "cx22"
}

variable "location" {
  description = "Localisation du serveur"
  type        = string
  default     = "fsn1"

  validation {
    condition     = contains(["fsn1", "nbg1", "hel1", "ash", "hil"], var.location)
    error_message = "Localisation invalide. Choix : fsn1, nbg1, hel1, ash, hil."
  }
}

variable "image" {
  description = "Image OS"
  type        = string
  default     = "ubuntu-24.04"
}

variable "ssh_key_ids" {
  description = "Liste des IDs de clés SSH"
  type        = list(number)
}

variable "labels" {
  description = "Labels Hetzner"
  type        = map(string)
  default     = {}
}

variable "firewall_ids" {
  description = "IDs des firewalls Ă  attacher"
  type        = list(number)
  default     = []
}

variable "network_id" {
  description = "ID du réseau privé (optionnel)"
  type        = number
  default     = null
}

variable "network_ip" {
  description = "IP dans le réseau privé (optionnel)"
  type        = string
  default     = null
}

variable "backups" {
  description = "Activer les backups automatiques"
  type        = bool
  default     = false
}

variable "user_data" {
  description = "Cloud-init user data"
  type        = string
  default     = null
}
# modules/hetzner-server/main.tf
resource "hcloud_server" "this" {
  name        = var.name
  server_type = var.server_type
  location    = var.location
  image       = var.image
  ssh_keys    = var.ssh_key_ids
  backups     = var.backups
  user_data   = var.user_data
  firewall_ids = var.firewall_ids

  labels = merge(
    {
      managed_by = "terraform"
      module     = "hetzner-server"
    },
    var.labels
  )

  lifecycle {
    ignore_changes = [
      user_data,  # Ne pas recréer si le user_data change
      ssh_keys,   # Géré en dehors
    ]
  }
}

# Réseau privé (optionnel)
resource "hcloud_server_network" "this" {
  count = var.network_id != null ? 1 : 0

  server_id  = hcloud_server.this.id
  network_id = var.network_id
  ip         = var.network_ip
}
# modules/hetzner-server/outputs.tf
output "id" {
  description = "ID du serveur"
  value       = hcloud_server.this.id
}

output "name" {
  description = "Nom du serveur"
  value       = hcloud_server.this.name
}

output "ipv4_address" {
  description = "Adresse IPv4 publique"
  value       = hcloud_server.this.ipv4_address
}

output "ipv6_address" {
  description = "Adresse IPv6"
  value       = hcloud_server.this.ipv6_address
}

output "status" {
  description = "Statut du serveur"
  value       = hcloud_server.this.status
}

output "private_ip" {
  description = "IP privée (si réseau configuré)"
  value       = var.network_id != null ? hcloud_server_network.this[0].ip : null
}

Appeler un module

# Depuis un répertoire local
module "web_server" {
  source = "./modules/hetzner-server"

  name        = "web-prod-01"
  server_type = "cx32"
  location    = "nbg1"
  ssh_key_ids = [hcloud_ssh_key.deploy.id]
  backups     = true
  network_id  = hcloud_network.prod.id
  network_ip  = "10.0.1.10"

  labels = {
    environment = "prod"
    role        = "web"
  }
}

# Depuis un repo Git (avec version tag)
module "database" {
  source = "git::https://github.com/devopslab/terraform-hetzner-server.git?ref=v1.2.0"

  name        = "db-prod-01"
  server_type = "cx42"
  location    = "nbg1"
  ssh_key_ids = [hcloud_ssh_key.deploy.id]
  backups     = true

  labels = {
    environment = "prod"
    role        = "database"
  }
}

# Depuis le Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true
}

# Utiliser les outputs du module
output "web_server_ip" {
  value = module.web_server.ipv4_address
}

Créer plusieurs instances avec for_each

locals {
  servers = {
    "web-01" = {
      server_type = "cx32"
      role        = "web"
      network_ip  = "10.0.1.10"
    }
    "web-02" = {
      server_type = "cx32"
      role        = "web"
      network_ip  = "10.0.1.11"
    }
    "api-01" = {
      server_type = "cx42"
      role        = "api"
      network_ip  = "10.0.2.10"
    }
    "worker-01" = {
      server_type = "cx22"
      role        = "worker"
      network_ip  = "10.0.3.10"
    }
  }
}

module "servers" {
  source   = "./modules/hetzner-server"
  for_each = local.servers

  name        = each.key
  server_type = each.value.server_type
  location    = "nbg1"
  ssh_key_ids = [hcloud_ssh_key.deploy.id]
  backups     = true
  network_id  = hcloud_network.prod.id
  network_ip  = each.value.network_ip

  labels = {
    environment = "prod"
    role        = each.value.role
  }
}

# Toutes les IPs publiques
output "all_server_ips" {
  value = {
    for name, server in module.servers :
    name => server.ipv4_address
  }
}

Versioning et publication

# Structure de repo pour un module public
terraform-hetzner-server/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
├── CHANGELOG.md
├── LICENSE
├── examples/
│   ├── basic/
│   │   └── main.tf
│   └── with-network/
│       └── main.tf
└── test/
    └── basic_test.go

# Versionner avec des tags Git
git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0

# Utiliser des contraintes de version sémantique
# version = "1.0.0"     # Exactement cette version
# version = "~> 1.0"    # >= 1.0, < 2.0 (minor + patch)
# version = ">= 1.0, < 1.5"  # Range explicite

Ce que tu vas apprendre dans ce module

  • ✅ Quand crĂ©er un module (et quand s’en abstenir)
  • ✅ La structure standard d’un module Terraform
  • ✅ Écrire des variables avec validation et des outputs utiles
  • ✅ Appeler un module depuis un chemin local, Git ou le Registry
  • ✅ Combiner for_each avec des modules pour des dĂ©ploiements dynamiques
  • ✅ Versionner tes modules avec le semantic versioning
  • ✅ Documenter un module pour qu’il soit utilisable par ton Ă©quipe
  • ✅ Tester un module avec Terratest ou terraform test

Module 5 — Production & CI/CD

Terraform en production : les vrais enjeux

Faire tourner Terraform sur ta machine, c’est bien pour apprendre. Mais en production, personne ne devrait faire terraform apply depuis son laptop. Les raisons sont multiples : traçabilitĂ©, reproductibilitĂ©, contrĂŽle d’accĂšs, revue de code, et surtout — rĂ©duire le risque d’erreur humaine.

La solution : intĂ©grer Terraform dans ton pipeline CI/CD. Le pattern standard est devenu un classique : terraform plan sur chaque pull request (pour que l’équipe voie les changements proposĂ©s), terraform apply automatique au merge sur main (pour que seul du code revu soit appliquĂ©). Simple, efficace, auditable.

Mais ça ne s’arrĂȘte pas lĂ . En 2026, les Ă©quipes matures ajoutent des couches de validation : linting avec tflint, formatting avec terraform fmt, sĂ©curitĂ© avec tfsec/trivy, et policies avec OPA ou Sentinel. Chaque PR passe par un pipeline complet avant que quiconque puisse merger.

Ce module te montre comment mettre tout ça en place avec GitHub Actions, mais les concepts sont transposables à GitLab CI, Azure DevOps, ou n’importe quel autre outil CI/CD.

Pipeline GitHub Actions complet

# .github/workflows/terraform.yml
name: "Terraform CI/CD"

on:
  pull_request:
    branches: [main]
    paths:
      - 'infrastructure/**'
  push:
    branches: [main]
    paths:
      - 'infrastructure/**'

permissions:
  contents: read
  pull-requests: write
  id-token: write  # Pour OIDC avec AWS

env:
  TF_VERSION: "1.9.8"
  WORKING_DIR: "infrastructure"
  TF_IN_AUTOMATION: "true"

jobs:
  # ─────────────────────────────────────────
  # Job 1 : Validation et linting
  # ─────────────────────────────────────────
  validate:
    name: "🔍 Validate & Lint"
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ env.WORKING_DIR }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check -recursive -diff
        continue-on-error: true

      - name: Terraform Init
        run: terraform init -backend=false

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: TFLint
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest

      - name: Run TFLint
        run: |
          tflint --init
          tflint --format compact --recursive

      - name: Trivy Security Scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: ${{ env.WORKING_DIR }}
          severity: 'HIGH,CRITICAL'
          exit-code: '1'

  # ─────────────────────────────────────────
  # Job 2 : Plan sur Pull Request
  # ─────────────────────────────────────────
  plan:
    name: "📋 Terraform Plan"
    runs-on: ubuntu-latest
    needs: validate
    if: github.event_name == 'pull_request'
    defaults:
      run:
        working-directory: ${{ env.WORKING_DIR }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      # AWS OIDC — pas de secrets statiques
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: eu-central-1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        id: plan
        run: |
          terraform plan -no-color -input=false -out=tfplan \
            2>&1 | tee plan_output.txt
        continue-on-error: true

      - name: Save Plan Artifact
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: ${{ env.WORKING_DIR }}/tfplan

      # Poster le plan en commentaire sur la PR
      - name: Comment PR with Plan
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync(
              '${{ env.WORKING_DIR }}/plan_output.txt', 'utf8'
            );
            const truncated = plan.length > 60000
              ? plan.substring(0, 60000) + '\n\n... (tronqué)'
              : plan;

            const body = `### 📋 Terraform Plan
            
            \`\`\`
            ${truncated}
            \`\`\`
            
            *Plan généré par le workflow CI/CD*`;

            // Supprimer les anciens commentaires de plan
            const comments = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            for (const comment of comments.data) {
              if (comment.body.includes('📋 Terraform Plan')) {
                await github.rest.issues.deleteComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: comment.id,
                });
              }
            }

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body,
            });

      - name: Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

  # ─────────────────────────────────────────
  # Job 3 : Apply au merge sur main
  # ─────────────────────────────────────────
  apply:
    name: "🚀 Terraform Apply"
    runs-on: ubuntu-latest
    needs: validate
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    defaults:
      run:
        working-directory: ${{ env.WORKING_DIR }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: eu-central-1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Apply
        run: terraform apply -auto-approve -input=false

Policies avec OPA (Open Policy Agent)

# policies/terraform.rego
package terraform

import rego.v1

# Interdire les serveurs sans labels obligatoires
deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "hcloud_server"
    resource.change.actions[_] == "create"
    
    labels := resource.change.after.labels
    not labels.environment
    msg := sprintf(
        "Le serveur '%s' doit avoir un label 'environment'",
        [resource.name]
    )
}

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "hcloud_server"
    resource.change.actions[_] == "create"
    
    labels := resource.change.after.labels
    not labels.managed_by
    msg := sprintf(
        "Le serveur '%s' doit avoir un label 'managed_by'",
        [resource.name]
    )
}

# Interdire les serveurs trop gros en dev
deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "hcloud_server"
    resource.change.actions[_] == "create"
    
    labels := resource.change.after.labels
    labels.environment == "dev"
    
    allowed_types := {"cx22", "cx32"}
    server_type := resource.change.after.server_type
    not allowed_types[server_type]
    
    msg := sprintf(
        "En dev, le serveur '%s' ne peut pas ĂȘtre de type '%s'. Max autorisĂ© : cx32",
        [resource.name, server_type]
    )
}

# Interdire la suppression de ressources en prod
deny contains msg if {
    resource := input.resource_changes[_]
    resource.change.actions[_] == "delete"
    
    labels := resource.change.before.labels
    labels.environment == "prod"
    
    msg := sprintf(
        "Suppression interdite pour la ressource prod '%s/%s'. Utilise un processus de décommissionnement.",
        [resource.type, resource.name]
    )
}
# Intégrer OPA dans le pipeline
# 1. Générer le plan en JSON
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# 2. Évaluer les policies
opa eval \
  --input tfplan.json \
  --data policies/ \
  --format pretty \
  'data.terraform.deny'

# 3. Échouer si des violations sont dĂ©tectĂ©es
VIOLATIONS=$(opa eval \
  --input tfplan.json \
  --data policies/ \
  --format json \
  'data.terraform.deny' | jq '.result[0].expressions[0].value | length')

if [ "$VIOLATIONS" -gt 0 ]; then
  echo "❌ $VIOLATIONS violation(s) de policy dĂ©tectĂ©e(s)"
  exit 1
fi

Bonnes pratiques de sécurité CI/CD

# Utiliser OIDC au lieu de secrets statiques
# AWS : pas de ACCESS_KEY_ID / SECRET_ACCESS_KEY dans les secrets GitHub

# Chiffrer les variables sensibles avec des variables d'environnement
# Ne JAMAIS mettre de secrets dans les fichiers .tf
variable "db_password" {
  type      = string
  sensitive = true  # Masqué dans les logs
}

# Utiliser des data sources pour récupérer les secrets
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/database/password"
}

resource "hcloud_server" "db" {
  name        = "db-prod"
  server_type = "cx42"
  image       = "ubuntu-24.04"
  location    = "nbg1"

  user_data = templatefile("${path.module}/templates/db-init.sh.tpl", {
    db_password = data.aws_secretsmanager_secret_version.db_password.secret_string
  })
}

Pre-commit hooks pour la qualité locale

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.96.3
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint
        args:
          - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl
      - id: terraform_docs
        args:
          - --hook-config=--path-to-file=README.md
          - --hook-config=--add-to-existing-file=true
          - --hook-config=--create-file-if-not-exist=true
      - id: terraform_trivy
        args:
          - --args=--severity=HIGH,CRITICAL
# Installation
pip install pre-commit
pre-commit install

# Lancer manuellement
pre-commit run --all-files

Structure de projet production-ready

infrastructure/
├── .github/
│   └── workflows/
│       └── terraform.yml
├── .pre-commit-config.yaml
├── .tflint.hcl
├── policies/
│   └── terraform.rego
├── modules/
│   ├── hetzner-server/
│   ├── hetzner-network/
│   └── cloudflare-dns/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── backend.tf       # State sĂ©parĂ© par env
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   ├── backend.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── backend.tf
│       ├── variables.tf
│       └── terraform.tfvars
└── README.md

Ce que tu vas apprendre dans ce module

  • ✅ Pourquoi terraform apply depuis un laptop est un anti-pattern
  • ✅ Mettre en place un pipeline CI/CD complet avec GitHub Actions
  • ✅ plan sur PR, apply au merge — le workflow standard
  • ✅ Poster le plan en commentaire sur la PR pour la revue
  • ✅ Écrire des policies OPA pour valider les plans automatiquement
  • ✅ Configurer OIDC pour Ă©viter les secrets statiques
  • ✅ Installer des pre-commit hooks pour la qualitĂ© locale
  • ✅ Structurer un projet Terraform pour la production
  • ✅ GĂ©rer les secrets proprement (Vault, AWS Secrets Manager)

Prérequis

Compétences techniques

Pour tirer le maximum de cette formation, tu devrais ĂȘtre Ă  l’aise avec :

  • Terminal / CLI — Tu passes du temps dans un terminal, tu connais les commandes de base (cd, ls, cat, grep, etc.)
  • Git — Tu sais cloner, brancher, committer, pousser. Les pull requests ne te font pas peur
  • Concepts cloud — Tu sais ce qu’est un serveur virtuel, un rĂ©seau, un firewall, un DNS. Pas besoin d’ĂȘtre expert, mais les bases sont nĂ©cessaires
  • Un Ă©diteur de code — VS Code avec l’extension HashiCorp Terraform est recommandĂ©, mais Neovim, IntelliJ ou n’importe quel Ă©diteur avec support HCL fonctionne

Environnement technique

# Ce dont tu as besoin :
# 1. Terraform ou OpenTofu installé
terraform version  # >= 1.7.0

# 2. Un compte sur au moins un cloud provider
#    - Hetzner Cloud (recommandĂ© pour dĂ©buter, ~4€/mois)
#    - AWS Free Tier
#    - GCP Free Tier
#    - Infomaniak (provider communautaire)

# 3. Git configuré
git --version

# 4. Un éditeur avec support HCL
code --install-extension hashicorp.terraform

# 5. (Optionnel) Des outils de qualité
brew install tflint    # Linter
brew install trivy     # Sécurité
brew install opa       # Policies
pip install pre-commit # Hooks

Ce qui n’est PAS requis

  • ❌ ExpĂ©rience prĂ©alable avec Terraform ou un autre outil IaC
  • ❌ Connaissance approfondie d’un cloud provider spĂ©cifique
  • ❌ ExpĂ©rience avec Go (le langage dans lequel Terraform est Ă©crit)
  • ❌ Un budget cloud important (les exemples utilisent des instances minimales)

Pourquoi cette formation ?

Le problÚme que tu résous

Tu gĂšres de l’infrastructure. Peut-ĂȘtre quelques serveurs, peut-ĂȘtre des dizaines. Et Ă  chaque fois que tu dois reproduire un environnement, migrer un service, ou revenir en arriĂšre aprĂšs un incident, c’est la galĂšre. Les Ă©tapes sont dans un wiki obsolĂšte, dans la tĂȘte de quelqu’un, ou nulle part.

Avec Terraform, ton infrastructure est :

  • 📝 DocumentĂ©e — Le code HCL EST la documentation
  • 🔄 Reproductible — terraform apply donne le mĂȘme rĂ©sultat Ă  chaque fois
  • 👀 Revue — Chaque changement passe par une PR, comme le code applicatif
  • 🕐 VersionnĂ©e — Git te donne l’historique complet de chaque modification
  • ⚡ Rapide — RecrĂ©er un environnement complet prend des minutes, pas des jours
  • đŸ§Ș Testable — Tu peux valider les changements avant de les appliquer (plan)

Ce qui rend cette formation différente

Il y a plein de tutos Terraform sur internet. La plupart te montrent comment crĂ©er un EC2 sur AWS et s’arrĂȘtent lĂ . Cette formation va plus loin :

  1. Multi-cloud rĂ©el — On utilise Hetzner, Infomaniak, AWS, et GCP. Pas juste AWS
  2. Patterns de production — CI/CD, policies, remote state, modules versionnĂ©s — ce que tu fais en vrai
  3. Code concret — Chaque concept est illustrĂ© par du HCL que tu peux copier et adapter
  4. Progressif — Du premier init au pipeline complet, chaque module construit sur le prĂ©cĂ©dent
  5. Opinionated — On te dit ce qui marche et ce qui ne marche pas, pas juste les options disponibles
  6. Suisse-friendly — Exemples avec Infomaniak et Hetzner, providers populaires dans nos contrĂ©es

Qui devrait suivre cette formation ?

  • SysAdmins qui veulent passer au DevOps et automatiser leur infrastructure
  • DĂ©veloppeurs qui veulent comprendre et gĂ©rer l’infra de leurs applications
  • DevOps Engineers qui utilisent Terraform mais veulent structurer leurs pratiques
  • Équipes qui migrent du clic-clic vers l’IaC et cherchent un cadre

Le résultat concret

À la fin de cette formation, tu seras capable de :

✅ DĂ©ployer n'importe quelle infrastructure avec Terraform
✅ Structurer un projet Terraform propre et maintenable
✅ GĂ©rer le state en remote avec locking et chiffrement
✅ CrĂ©er des modules rĂ©utilisables pour ton Ă©quipe
✅ Mettre en place un pipeline CI/CD complet
✅ Appliquer des policies de sĂ©curitĂ© automatisĂ©es
✅ Passer d'un provider à l'autre sans changer de workflow

PrĂȘt Ă  commencer ?

La meilleure façon d’apprendre Terraform, c’est de l’utiliser. Commence par le Module 1, installe Terraform, et dĂ©ploie ton premier serveur. En 30 minutes, tu auras ta premiĂšre infrastructure codĂ©e.

Commence maintenant → Chapitre 1 : Fondamentaux de Terraform

Ressources complémentaires

RessourceLien
Documentation officielle Terraformdeveloper.hashicorp.com/terraform
Documentation OpenTofuopentofu.org/docs
Terraform Registry (providers & modules)registry.terraform.io
Hetzner Cloud Providerregistry.terraform.io/providers/hetznercloud/hcloud
Awesome Terraform (curated list)github.com/shuaibiyy/awesome-terraform
Terraform Best Practicesterraform-best-practices.com

Formation créée par DevOpsLab.ch — Infrastructure as Code, automatisation et bonnes pratiques DevOps.