đïž Terraform â Infrastructure as Code
Programme
Pourquoi l'IaC ? Introduction Ă Terraform Gratuit
Découvre l'Infrastructure as Code, installe Terraform, apprends la syntaxe HCL, les providers et réalise ton premier déploiement sur Hetzner.
Init, Plan, Apply : le workflow Terraform
Maßtrise le workflow Terraform complet : init, plan, apply, destroy. Puis découvre les resources, data sources, le state et la structure de projet.
Variables et types de données
Maßtrise les variables d'entrée Terraform, les différents types (string, number, list, map, object), l'assignation de valeurs et les outputs.
Locals, outputs et expressions
Boucles count et for_each, blocs dynamiques, expressions conditionnelles et fonctions built-in Terraform pour du code DRY.
Le State : comprendre l'état Terraform
Comprends le state Terraform : fichier local vs backend distant (S3, Consul), locking, import de ressources et bonnes pratiques de gestion du state.
Modules : structurer son code
Crée des modules Terraform réutilisables, adopte les bonnes pratiques de structure de projet et prépare ton code pour la production.
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
| Aspect | Terraform | OpenTofu |
|---|---|---|
| Licence | BSL 1.1 (non open source) | MPL 2.0 (open source) |
| Mainteneur | HashiCorp (IBM) | Linux Foundation |
| CLI | terraform | tofu |
| Registry | registry.terraform.io | registry.opentofu.org |
| Compatibilité HCL | Référence | Compatible ~99% |
| State encryption | Non natif | Natif depuis 1.7 |
| Features exclusives | Moved blocks, checks | Early 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 :
- đ Chapitre 1 â Fondamentaux de Terraform : Installation, premiers providers, workflow de base
- đ Chapitre 2 â Variables et expressions HCL : Variables, outputs, boucles, conditions
- đ 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_eachet les expressionsfor - â 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
localspour é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,showetlist - â 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_eachavec 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 applydepuis un laptop est un anti-pattern - â Mettre en place un pipeline CI/CD complet avec GitHub Actions
- â
plansur PR,applyau 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 applydonne 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 :
- Multi-cloud rĂ©el â On utilise Hetzner, Infomaniak, AWS, et GCP. Pas juste AWS
- Patterns de production â CI/CD, policies, remote state, modules versionnĂ©s â ce que tu fais en vrai
- Code concret â Chaque concept est illustrĂ© par du HCL que tu peux copier et adapter
- Progressif â Du premier
initau pipeline complet, chaque module construit sur le prĂ©cĂ©dent - Opinionated â On te dit ce qui marche et ce qui ne marche pas, pas juste les options disponibles
- 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
| Ressource | Lien |
|---|---|
| Documentation officielle Terraform | developer.hashicorp.com/terraform |
| Documentation OpenTofu | opentofu.org/docs |
| Terraform Registry (providers & modules) | registry.terraform.io |
| Hetzner Cloud Provider | registry.terraform.io/providers/hetznercloud/hcloud |
| Awesome Terraform (curated list) | github.com/shuaibiyy/awesome-terraform |
| Terraform Best Practices | terraform-best-practices.com |
Formation créée par DevOpsLab.ch â Infrastructure as Code, automatisation et bonnes pratiques DevOps.