đïž Terraform â Infrastructure as Code (IaC) & Cloud
Guide complet IDEOâLab sur l'outil IaC (Core, HCL, State, Providers, Modules).
Concept : IaC
Infrastructure as Code. Déclaratif (Terraform) vs Impératif (Ansible).
IaC DéclaratifArchitecture
Core (CLI), Providers (AWS, GCP, ...), State File.
Core ProvidersInstallation
tfenv (gestionnaire de versions, reco) ou binaire manuel.
Le Workflow Essentiel
init (Initier), plan (Prévoir), apply (Appliquer), destroy.
HCL : Syntaxe (.tf)
provider, resource, data, variable, output.
Fichier d'Ătat Local (.tfstate)
Le "cerveau" de Terraform. .tfstate, .gitignore.
Ătat distant (Backend)
backend "s3", S3 (HA) + DynamoDB (Locking). Indispensable en équipe.
Providers (AWS, GCP, ...)
Configuration des "plugins" (AWS, GCP, Azure, Docker...).
provider AWS GCPVariables (Input)
variable {}, terraform.tfvars, -var, TF_VAR_.
Outputs (Sorties)
output {} (ex: aws_instance.web.public_ip).
Exemple: Instance AWS (EC2)
aws_instance, aws_security_group (Firewall HTTP/SSH).
Exemple: Instance GCP (GCE)
google_compute_instance, google_compute_firewall.
Modules (Réutilisabilité)
module {}, source (Registry, Git), variables.tf.
Data Sources (Lecture)
data "aws_vpc" ..., lire l'infra existante.
Provisioners (à éviter)
remote-exec (post-install), l'anti-pattern (utiliser Ansible).
Cheat-sheet (CLI & State)
init, plan, apply, fmt, validate, state list.
Infrastructure as Code (IaC)
L'IaC est la pratique de gestion et de provisionnement de l'infrastructure (serveurs, BDD, réseaux, DNS...) via des **fichiers de configuration lisibles par l'homme** (ex: HCL, YAML), plutÎt que par une configuration manuelle (clics dans la console AWS/GCP) ou des scripts impératifs.
Avantages :
- Reproductibilité : Vous pouvez détruire et recréer votre infra 100 fois à l'identique.
- Versionning : Votre infra est dans Git. Vous pouvez voir qui a changé quoi (
git blame). - CI/CD : L'infra est déployée automatiquement (ex: "plan" sur une Pull Request).
Déclaratif (Terraform) vs. Impératif (Ansible)
C'est la distinction la plus importante.
| ModĂšle | Analogie (Le "Quoi") | Analogie (Le "Comment") | Outil |
|---|---|---|---|
| DĂ©claratif (Ătat) | "Je veux 3 serveurs Nginx (Instance A, B, C)." | (Terraform compare l'Ă©tat actuel et l'Ă©tat dĂ©sirĂ©. Il voit que B manque, il le crĂ©e.) | Terraform, Kubernetes, Pulumi |
| Impératif (Action) | "Crée le serveur A. Crée le serveur B. Crée le serveur C." | (Ansible exécute les 3 commandes. S'il est relancé, il essaie de les re-créer (sauf si state=present est géré).) | Ansible, Chef, Puppet, Scripts Bash |
Conclusion : Terraform est pour **provisionner** l'infrastructure (le "matériel"). Ansible est pour **configurer** l'infrastructure (le "logiciel"). (Ils sont complémentaires !)
Les 3 Composants Clés
- 1. Terraform Core (CLI) : Le binaire (
terraform) écrit en Go. Il lit les fichiers.tf, construit le graphe de dépendances, et exécute le "plan". - 2. Providers (Plugins) : Les "traducteurs". Ce sont des binaires (plugins) qui font le lien entre HCL (le langage) et l'API du service cible (AWS, GCP, Cloudflare, Docker...).
- 3. State File (Fichier d'Ătat) : Le "cerveau". Un fichier JSON (
terraform.tfstate) qui stocke l'état *actuel* de l'infrastructure gérée. (Voir 2.2 & 2.3).
Schéma de flux (terraform apply)
+-----------------------+
| Fichiers .tf (HCL) |
| (Ătat dĂ©sirĂ©) |
+-----------------------+
|
âŒ
+-----------------------+
| Terraform Core (CLI) |
| (GénÚre le "Plan") |
+-----------------------+
| |
(Compare) | (Appelle)
| âŒ
| +--------------+
| | Provider AWS | (Plugin)
| +--------------+
| |
| ⌠(Appels API)
| [Infrastructure Cloud (AWS)]
|
âŒ
+-----------------------+
| State File (.tfstate) |
| (Ătat actuel) |
+-----------------------+
tfenv ou Manuelle)tfenv (Gestionnaire de versions)
ProblÚme : Les projets Terraform sont sensibles à la version (ex: 1.5 vs 1.6). tfenv (comme nvm pour Node) permet de gérer plusieurs versions.
# 1. Installer tfenv (ex: macOS) brew install tfenv # (ex: Linux) git clone https://github.com/tfutils/tfenv.git ~/.tfenv # (Ajouter ~/.tfenv/bin au PATH) # 2. Installer une version de Terraform tfenv install 1.8.0 tfenv install latest # 3. Utiliser une version (globale) tfenv use 1.8.0 # (Utiliser par projet) # (Crée un .terraform-version à la racine du projet) tfenv use 1.8.0
Installation Manuelle (Binaire)
Terraform est un binaire Go unique.
# (Adapté pour Linux)
# 1. Télécharger (HashiCorp)
VERSION="1.8.0"
wget https://releases.hashicorp.com/terraform/${VERSION}/terraform_${VERSION}_linux_amd64.zip
# 2. Dézipper
unzip terraform_${VERSION}_linux_amd64.zip
# 3. Déplacer dans le PATH
sudo mv terraform /usr/local/bin/
# 4. Vérifier
terraform --versionterraform init
Ă lancer une fois par projet (ou si les providers changent).
Action : Lit les provider {} et module {} dans vos fichiers .tf et télécharge les plugins nécessaires (ex: provider-aws) dans un dossier .terraform/.
$ terraform init Initializing the backend... Initializing provider plugins...
terraform plan
(Recommandé) Un "dry-run" (simulation).
Action : Compare votre code (.tf) à l'état (.tfstate) et aux ressources réelles (API Cloud). Il vous montre ce qu'il *va* faire (Créer, Modifier, Détruire).
$ terraform plan ... Plan: 1 to add, 0 to change, 0 to destroy. + create (A green '+' = Création)
terraform apply
Action : Exécute le plan. (Il vous montre le "plan" une derniÚre fois et demande confirmation "yes").
$ terraform apply ... Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? [yes/no] yes Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
terraform destroy
(Dangereux) L'inverse d'apply.
Action : Lit l'état (.tfstate) et détruit toutes les ressources qu'il gÚre.
$ terraform destroy Plan: 0 to add, 0 to change, 1 to destroy. ... Destroy complete! Resources: 1 destroyed.
.tf)HCL (HashiCorp Configuration Language) est le langage de Terraform. Il est déclaratif.
# main.tf
# 1. Définir le provider (plugin) requis
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# 2. Configurer le provider
provider "aws" {
region = "eu-west-3"
}
# 3. Définir une Variable (Input)
variable "instance_type" {
description = "Taille de l'instance EC2"
type = string
default = "t2.micro"
}
# 4. Définir une Ressource (L'infra !)
# resource "[TYPE]" "[NOM_LOCAL]"
resource "aws_instance" "web_server" {
ami = "ami-0abcdef123"
instance_type = var.instance_type # Référence à la variable
tags = {
Name = "Serveur Web (Terraform)"
}
}
# 5. Définir une Sortie (Output)
output "instance_ip" {
description = "IP Publique de l'instance"
value = aws_instance.web_server.public_ip
}.tfstate) (Crucial)Le "Cerveau" de Terraform
Le fichier terraform.tfstate est un JSON généré par terraform apply. C'est la partie la plus importante (et la plus dangereuse) de Terraform.
RÎle : Il agit comme une "base de données" qui mappe votre code (resource "aws_instance" "web") à l'ID de la ressource réelle ("i-123abc456").
Quand vous lancez plan, Terraform compare .tf (désiré) à .tfstate (actuel) pour savoir quoi faire (créer, modifier, détruire).
.gitignore (CRITIQUE)
â ïž Le .tfstate local contient des secrets (mots de passe BDD, clĂ©s API...) en CLAIR.
Il ne doit **JAMAIS** ĂȘtre commitĂ© (push) sur Git.
# .gitignore *.tfstate *.tfstate.backup .terraform/ terraform.tfvars
ProblÚme : Si vous le perdez, Terraform "oublie" tout ce qu'il a créé. Si vous travaillez en équipe, comment partager l'état ? (Voir 2.3).
Backend (Ătat distant)
(Indispensable pour les équipes) Le "backend" configure Terraform pour qu'il stocke le .tfstate non pas localement, mais dans un stockage distant (ex: S3, GCP Bucket, Azure Blob).
Avantages :
- Partage : L'Ă©quipe partage le mĂȘme Ă©tat.
- Sécurité : Les secrets sont chiffrés (ex: S3 Encryption).
# main.tf
terraform {
backend "s3" {
# 1. Bucket S3 oĂč stocker le .tfstate
bucket = "mon-bucket-terraform-state-ideo"
# 2. Chemin du fichier
key = "production/terraform.tfstate"
# 3. Région du bucket
region = "eu-west-3"
}
}
# (AprĂšs ajout, relancer 'terraform init')State Locking (Verrouillage)
ProblĂšme : Que se passe-t-il si 2 dĂ©veloppeurs lancent terraform apply en mĂȘme temps ? (Conflit, corruption de l'Ă©tat).
Solution : Le "Locking". Avant de faire apply, Terraform "verrouille" l'état. Si un 2Úme apply est lancé, il échoue.
Pour AWS (S3), le locking se fait via une table DynamoDB.
# main.tf
terraform {
backend "s3" {
bucket = "mon-bucket-terraform-state-ideo"
key = "production/terraform.tfstate"
region = "eu-west-3"
# 4. (Verrouillage) Nom de la table DynamoDB
dynamodb_table = "terraform-lock-table"
}
}
# (La table DynamoDB doit ĂȘtre créée au prĂ©alable)AWS (Amazon Web Services)
Terraform utilise les credentials standards (~/.aws/credentials, ou variables d'env AWS_ACCESS_KEY_ID).
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-3" // Paris
default_tags {
tags = {
Owner = "IDEO-Lab"
Project = "Terraform Guide"
}
}
}GCP (Google Cloud Platform)
Terraform utilise un "Service Account Key File" (JSON).
# main.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = "ideo-lab-project-id"
region = "europe-west1"
zone = "europe-west1-b"
credentials = file("/path/to/service-account.json")
}variables.tf (Déclaration)
On déclare les "inputs" du projet (ex: ce qui change entre 'dev' et 'prod').
variable "env" {
description = "Environnement (dev, staging, prod)"
type = string
default = "dev"
}
variable "instance_type" {
description = "Taille de l'instance EC2"
type = string
default = "t2.micro"
}
variable "ami_id" {
description = "AMI Ă utiliser (ex: Ubuntu)"
type = string
# (Pas de 'default' = Requis)
}
variable "tags" {
description = "Tags Ă appliquer"
type = map(string)
default = {}
}Assignation (Priorité)
Comment Terraform assigne les valeurs (du plus bas au plus haut) :
- (Bas)
default:(dansvariable {}) *.auto.tfvars(Fichiers chargés auto)terraform.tfvars(Fichier de dev local)-var="ami_id=ami-123"(Flag CLI)- (Haut)
TF_VAR_ami_id=ami-123(Variable d'env)
terraform.tfvars (Dev local)
(Fichier Ă ignorer dans .gitignore)
# terraform.tfvars # (Assignations pour 'ami_id' (requis)) ami_id = "ami-0abcdef123" instance_type = "t3.small"
Les "Outputs" (Sorties) affichent des informations utiles (ex: l'IP du serveur) à la fin d'un apply. Elles sont aussi utilisées pour lire les données d'un "Module" (voir 4.3).
outputs.tf
output "instance_ip" {
description = "IP Publique de l'instance web"
# (Référence à la ressource "aws_instance" nommée "web_server")
value = aws_instance.web_server.public_ip
}
output "instance_id" {
description = "ID de l'instance"
value = aws_instance.web_server.id
}Résultat (Fin de terraform apply)
Apply complete! Outputs: instance_id = "i-1234567890abcdef0" instance_ip = "54.123.45.67"
Crée un "Security Group" (Firewall) et une Instance (VM) Ubuntu pour un serveur Django/Nginx.
# --- 1. Security Group (Firewall) ---
resource "aws_security_group" "django_sg" {
name = "django-sg"
description = "Autorise HTTP(S) et SSH"
# (Entrée) Port 22 (SSH) (depuis votre IP)
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["80.1.2.3/32"] # (Votre IP)
}
# (Entrée) Port 80 (HTTP)
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# (Sortie) Tout autorisé
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# --- 2. Instance EC2 (VM) ---
resource "aws_instance" "django_server" {
ami = "ami-0fc5d9351a0b0b8e1" # (Ubuntu 22.04 - eu-west-3)
instance_type = "t3.micro"
# (Lier le Firewall)
vpc_security_group_ids = [aws_security_group.django_sg.id]
# (Lier la clé SSH (doit exister dans AWS))
key_name = "ma-cle-ssh-aws"
tags = {
Name = "Django Server (Prod)"
}
}Crée une rÚgle de Firewall et une Instance (VM) Debian.
# --- 1. Firewall (HTTP/SSH) ---
resource "google_compute_firewall" "django_firewall" {
name = "allow-http-ssh"
network = "default"
allow {
protocol = "tcp"
ports = ["22", "80"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["web-server"]
}
# --- 2. Instance GCE (VM) ---
resource "google_compute_instance" "django_server" {
name = "django-server-prod"
machine_type = "e2-micro"
zone = "europe-west1-b"
# (Appliquer le tag du firewall)
tags = ["web-server"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
access_config {
# (Donne une IP publique)
}
}
metadata = {
# (Ajoute votre clé SSH à l'instance)
ssh-keys = "ideo_user:${file("~/.ssh/id_rsa.pub")}"
}
}Concept (DRY - Don't Repeat Yourself)
ProblĂšme : Vous avez 10 projets Django. Vous devez copier/coller le code (EC2 + SG) 10 fois. Si vous devez mettre Ă jour le firewall, vous modifiez 10 fichiers.
Solution : Un **Module** est un "composant" Terraform réutilisable (l'équivalent d'un "Role" Ansible ou d'une "Fonction").
Utilisation (main.tf du projet)
On "appelle" le module (ex: un module VPC) et on lui passe des variables.
# --- 1. Appel d'un module (Registry) ---
module "vpc_prod" {
# Source: Terraform Registry
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
# (Variables du module)
name = "vpc-prod"
cidr = "10.0.0.0/16"
}
# --- 2. Appel d'un module (Git/Local) ---
module "serveur_django" {
source = "./modules/ec2-django"
# (ou 'git::https://.../tf-modules.git//ec2-django')
env = "prod"
ami_id = "ami-123"
subnet_id = module.vpc_prod.public_subnets[0]
}Structure (./modules/ec2-django/)
Un module est un dossier Terraform standard.
modules/ec2-django/ âââ main.tf (Les 'resource "aws_instance"...') âââ variables.tf (Les 'inputs', ex: var.env) âââ outputs.tf (Les 'outputs', ex: output "ip" ...)
data (Lecture de l'existant)
ProblÚme : Votre VPC a été créé manuellement (ou par un autre projet Terraform). Comment obtenir son ID ?
Solution : Utiliser un bloc data. C'est une requĂȘte en "lecture seule" (Read-Only) Ă l'API du Provider.
Exemple (data)
# (On suppose que le VPC "ideo-vpc" existe déjà )
data "aws_vpc" "existing_vpc" {
filter {
name = "tag:Name"
values = ["ideo-vpc"]
}
}
# (On suppose que l'AMI Ubuntu existe)
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-*"
values = ["099720109477"] # (ID du Propriétaire Canonical)
}
}
resource "aws_instance" "web" {
# Utiliser les données lues
ami = data.aws_ami.ubuntu.id
vpc_id = data.aws_vpc.existing_vpc.id
...
}remote-exec) (Anti-Pattern)Concept (à éviter)
Les "Provisioners" (remote-exec, local-exec) sont une "porte de sortie" pour exécuter des scripts impératifs (ex: Bash) aprÚs la création d'une ressource.
â ïž Anti-Pattern : C'est considĂ©rĂ© comme une mauvaise pratique. Terraform est dĂ©claratif (Ătat). Si le script remote-exec Ă©choue, l'Ă©tat de Terraform est "contaminĂ©" (il ne sait pas ce qui a Ă©tĂ© fait).
La "Bonne" Façon (Terraform + Ansible)
1. Terraform (main.tf) : Provisionne l'infra (EC2, SG) et génÚre un inventaire.
resource "aws_instance" "web" { ... }
output "inventory" {
value = templatefile("inventory.ini.tpl", {
ip = aws_instance.web.public_ip
})
}
# (terraform apply -> inventory.ini)2. Ansible (playbook.yml) : Configure le logiciel (Nginx, Gunicorn, Django) sur l'IP générée.
ansible-playbook -i inventory.ini deploy.yml
Workflow (Essentiel)
# 1. Initialiser (Télécharge les providers) terraform init # 2. Formater (Nettoie le code HCL) terraform fmt # 3. Valider (Vérifie la syntaxe) terraform validate # 4. Planifier (Simulation / Dry-run) terraform plan # (Sauver le plan) terraform plan -out=mon_plan.tfplan # 5. Appliquer (Créer / Mettre à jour) terraform apply terraform apply "mon_plan.tfplan" # (Appliquer sans demande de confirmation) terraform apply -auto-approve # 6. Détruire (Tout supprimer) terraform destroy
Gestion de l'Ătat (.tfstate)
# Lister les ressources (selon .tfstate) terraform state list # Voir une ressource terraform state show aws_instance.web_server # (Avancé) Retirer une ressource de l'état # (Ne détruit pas la ressource, Terraform l'oublie) terraform state rm aws_instance.web_server # (Avancé) Importer une ressource (créée manuellement) # terraform import [ADRESSE_HCL] [ID_CLOUD] terraform import aws_instance.web_server i-123abc456 # (Avancé) Déplacer une ressource terraform state mv aws_instance.web aws_instance.old_web
