Terraform expliqué de A à Z : décrire son infrastructure en code, sans se piéger

DevOps Infrastructure Infrastructure as Code Terraform
Terraform expliqué de A à Z : décrire son infrastructure en code, sans se piéger

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, import pour ç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 destroy raté en dev ne doit jamais pouvoir toucher la prod.
  • Des versions de providers fixées (~> 5.0) et le fichier .terraform.lock.hcl commité.
  • 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 fmt et terraform validate dans 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.

Retour aux articles DevOps