Vous connaissez sûrement ce moment : il faut recréer un serveur identique à celui qui tourne depuis deux ans. Vous ouvrez la console de l'hébergeur, et vous essayez de vous souvenir. Quelle taille de disque ? Quelles règles de pare-feu ? Ce groupe de sécurité, il sert à quoi déjà ? Personne n'a rien noté, ou alors dans un wiki qui n'a pas été mis à jour depuis.
C'est exactement le problème que règle Terraform. Au lieu de cliquer, on décrit l'infrastructure dans des fichiers texte, rangés dans Git. Et c'est Terraform qui la construit, la modifie ou la détruit pour coller à ce qui est écrit. Voici le cours complet, avec la vidéo et des exemples prêts à copier.
L'infrastructure en tant que code
L'idée est simple : votre infrastructure (serveurs, réseaux, bases de données, DNS, buckets…) devient du code. Ce code est versionné dans Git, relu en merge request, testé, et rejouable autant de fois que nécessaire. Le jour où il faut recréer un environnement, on ne fouille plus sa mémoire : on relance le code.
Terraform, créé par HashiCorp, utilise un langage déclaratif appelé HCL (HashiCorp Configuration Language). Lisible, pas trop verbeux, et suffisamment expressif pour les cas complexes. À noter : depuis le changement de licence de Terraform en 2023, il existe un fork open source compatible, OpenTofu. Tout ce qui suit s'applique aux deux.
Les providers : les traducteurs
Terraform ne sait rien faire tout seul. Pour parler à AWS, Azure, Google Cloud, OVH, Scaleway, Cloudflare, Proxmox ou même GitLab, il utilise des providers : des plugins qui traduisent votre code en appels d'API.
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-3" # Paris
}
La ressource : la brique de base
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-prod"
}
}
On lit : une ressource de type aws_instance, que j'appelle web dans mon code, avec ces réglages. Le couple aws_instance.web devient son adresse unique dans le projet.
Le cycle en quatre commandes
terraform init # télécharge les providers, prépare le backend
terraform plan # montre ce qui va changer
terraform apply # applique (après confirmation)
terraform destroy # supprime tout ce que gère le projet
plan : votre filet de sécurité
Je ne lance jamais un apply sans avoir lu le plan. Il affiche, ressource par ressource :
+ aws_instance.web will be created
~ aws_security_group.web will be updated in-place
- aws_s3_bucket.old will be destroyed
-/+ aws_db_instance.main must be replaced
Le -/+ est celui qui doit vous faire lever les yeux : la ressource va être détruite puis recréée. Sur une base de données, ça veut dire perdre les données si vous n'avez pas fait attention. Certains changements, anodins en apparence (un nom, une zone de disponibilité), forcent un remplacement.
apply : passer à l'action
apply recalcule le plan, vous le montre, et attend un yes. En automatisation, on enregistre le plan validé et on applique exactement celui-là :
terraform plan -out=tfplan
terraform apply tfplan
Déclaratif : on décrit le résultat, pas les étapes
C'est la différence fondamentale avec un script. Vous ne dites pas « crée un serveur, puis change sa taille ». Vous dites « je veux un serveur de cette taille ». Si vous passez t3.micro à t3.small dans le code, Terraform compare avec l'existant et propose uniquement la modification nécessaire. Relancez apply dix fois sans changer le code : il ne se passe rien. C'est ce qu'on appelle l'idempotence.
Le state : la mémoire de Terraform
Comment Terraform sait-il que aws_instance.web correspond à l'instance i-0a1b2c3d chez AWS ? Grâce au state, un fichier JSON (terraform.tfstate) qui fait le lien entre chaque bloc de votre code et la vraie ressource.
Trois règles que je ne transgresse jamais :
- on ne le modifie jamais à la main (il existe
terraform state mv,rm,importpour ça) ; - on ne le commite jamais dans Git : il peut contenir des mots de passe en clair ;
- on ne le perd jamais : sans lui, Terraform ne sait plus ce qu'il gère.
Le backend distant
En équipe, le state ne peut pas rester sur l'ordinateur de quelqu'un. On le range dans un backend distant, avec un verrou pour empêcher deux apply simultanés :
terraform {
backend "s3" {
bucket = "mon-entreprise-tfstate"
key = "prod/reseau/terraform.tfstate"
region = "eu-west-3"
encrypt = true
use_lockfile = true # verrou natif S3 (Terraform 1.10+)
}
}
Sur les versions plus anciennes, le verrou passe par une table DynamoDB (dynamodb_table). D'autres backends existent : GitLab (très pratique si vous y êtes déjà), Azure Storage, GCS, ou HCP Terraform.
Variables, outputs, data sources
# variables.tf
variable "instance_type" {
type = string
default = "t3.micro"
description = "Taille de l'instance web"
}
# data source : lire sans gérer
data "aws_ami" "debian" {
most_recent = true
owners = ["136693071363"] # Debian
filter {
name = "name"
values = ["debian-12-amd64-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.debian.id
instance_type = var.instance_type
}
# outputs.tf
output "ip_publique" {
value = aws_instance.web.public_ip
}
Les variables rendent le code réutilisable entre environnements. Les outputs affichent (ou transmettent à d'autres projets) des informations utiles après création. Et les data sources lisent quelque chose qui existe déjà, sans le prendre en charge : ici, la dernière image Debian 12, plutôt qu'un identifiant d'image codé en dur qui finira par être obsolète.
Niveau expert
for_each : répéter sans copier-coller
resource "aws_instance" "web" {
for_each = toset(["front", "api", "worker"])
ami = data.aws_ami.debian.id
instance_type = "t3.small"
tags = { Name = each.key }
}
Trois noms, trois serveurs. J'utilise for_each plutôt que count : avec count, les ressources sont indexées par position (0, 1, 2). Retirez le premier élément de la liste, et Terraform veut détruire et recréer les suivants parce que leurs index ont changé. Avec for_each, chaque ressource est identifiée par sa clé et reste stable.
Le drift et l'import
Quelqu'un a modifié une règle de pare-feu à la main dans la console, « juste pour tester » ? Le code et la réalité ne correspondent plus : c'est le drift. Le prochain plan le détecte et propose de revenir à ce que dit le code. D'où l'importance de faire passer toutes les modifications par Terraform.
Et pour reprendre la main sur une ressource créée à la main, on l'importe dans le state. Depuis Terraform 1.5, ça se fait directement dans le code :
import {
to = aws_s3_bucket.logs
id = "mon-bucket-de-logs"
}
Combiné à terraform plan -generate-config-out=generated.tf, Terraform écrit même le bloc de ressource correspondant pour vous.
Les bonnes pratiques que j'applique
- Un state par environnement (dev, staging, prod) : un
destroyraté en dev ne doit jamais pouvoir toucher la prod. - Des versions de providers fixées (
~> 5.0) et le fichier.terraform.lock.hclcommité. - Aucun secret dans le code : variables d'environnement
TF_VAR_*ou coffre-fort comme Vault. - Plan automatique en merge request, apply après validation : le plan est relu comme du code.
terraform fmtetterraform validatedans la CI, systématiquement.
À retenir
On décrit l'infrastructure en code. init pour préparer, plan pour vérifier, apply pour appliquer. Le state garde la mémoire, rangé dans un backend distant et verrouillé. Et tout ce qui est modifié à la main finira par ressortir en drift.
La suite logique : arrêter de répéter le même code entre projets en le regroupant dans un module. C'est le sujet de mon article suivant, où l'on construit un module complet pour HashiCorp Vault.