L'idée centrale
👉 Terraform sert à décrire ce que tu veux, pas comment le faire.
Tu dis :
"Je veux que ça ressemble à ça."
Terraform se débrouille.
Sans Terraform
Tu veux un serveur :
- Tu cliques
- Tu choisis des options
- Tu oublies lesquelles
- Personne ne peut refaire pareil
- Si ça casse → panique
Avec Terraform (propre)
Tu écris un fichier texte :
Je veux :
- 1 serveur
- il s'appelle web
- il écoute sur le port 80
Et tu peux dire :
- Refais-le
- Détruis-le
- Change-le
- Recrée-le
➡️ Sans réfléchir, sans mémoire humaine
Les 5 mots-clés Terraform (ultra simples)
| Mot | Image mentale | Description |
|---|---|---|
| Provider | Qui fait le boulot | AWS, Azure, GCP, local, etc. |
| Resource | Ce que tu veux | Serveur, fichier, réseau, etc. |
| State | Le carnet de chantier | Mémoire de ce qui existe |
| Plan | Ce qui va changer | Différence entre voulu et réel |
| Apply | Exécuter | Appliquer les changements |
Ce qui se passe vraiment
- Tu écris ce que tu veux → Fichiers
.tf - Terraform regarde ce qui existe → Lit le state
- Il calcule la différence →
terraform plan - Il applique juste ce qu'il faut →
terraform apply
👉 Jamais tout casser pour rien
Exemple concret (local, simple)
resource "local_file" "exemple" {
filename = "bonjour.txt"
content = "Bonjour"
}
👉 Tu ne dis PAS :
- "crée un fichier"
- "ouvre le disque"
Tu dis :
"Je veux ce fichier avec ce contenu"
Terraform gère le reste.
Pourquoi les entreprises adorent ça
- ✅ 1 infra = 1 repo Git → Versionnement complet
- ✅ Historique clair → Qui a fait quoi et quand
- ✅ Rollback possible → Retour en arrière facile
- ✅ Zéro "c'était comment déjà ?" → Tout est documenté dans le code
Installation de Terraform
Sur macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Sur Debian/Ubuntu
# Installer GPG si nécessaire
sudo apt update
sudo apt install -y gpg
# Ajouter la clé GPG de HashiCorp
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Ajouter le dépôt HashiCorp
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Mettre à jour et installer Terraform
sudo apt update && sudo apt install terraform
Sur Linux (méthode manuelle)
Si vous préférez installer manuellement :
# Télécharger la dernière version
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
Sur Windows
Téléchargez depuis terraform.io/downloads ou utilisez Chocolatey :
choco install terraform
Vérifier l'installation
terraform version
Structure d'un projet Terraform
📁 Le dossier du projet
terraform-local-tp/
├── main.tf # Ressources principales
├── variables.tf # Variables d'entrée
├── outputs.tf # Valeurs de sortie
├── terraform.tfstate # État (généré automatiquement)
└── modules/ # Modules réutilisables
├── network/
└── compute/
👉 C'est le classeur du chantier
Modules = plans de pièces
Module network
# modules/network/main.tf
resource "local_file" "vpc_config" {
filename = "vpc.txt"
content = "VPC: ${var.vpc_name}"
}
👉 Plan du terrain :
- VPC (Virtual Private Cloud) : C'est comme délimiter ton terrain privé dans le cloud. Un VPC est un réseau virtuel isolé où tu vas placer tes ressources (serveurs, bases de données, etc.). C'est l'équivalent d'un réseau local privé, mais dans le cloud. Tu peux définir des règles de sécurité, des plages d'adresses IP, et contrôler qui peut communiquer avec quoi.
- Subnet : Ce sont des sous-réseaux à l'intérieur du VPC. Comme diviser ton terrain en zones (zone publique pour les serveurs web accessibles depuis Internet, zone privée pour les bases de données non accessibles directement).
- (en local : fichiers texte)
Module compute
# modules/compute/main.tf
resource "local_file" "server_config" {
filename = "server.txt"
content = "Server: ${var.server_name}"
}
👉 Plan de la maison :
- Serveur
- Installation Nginx (simulée)
➡️ Un module = un plan réutilisable
Variables = choix du client
# variables.tf
variable "environment" {
description = "Environnement de déploiement"
type = string
default = "dev"
}
variable "server_count" {
description = "Nombre de serveurs"
type = number
default = 1
}
# terraform.tfvars
environment = "dev"
server_count = 2
👉 Comme dire :
- "c'est un chantier de test"
- "ou c'est la vraie maison"
Environnements (dev / prod)
Même plan Mais :
- Matériaux différents
- Finitions différentes
👉 Même code, résultats adaptés
Exemple avec workspaces
# Créer un workspace pour dev
terraform workspace new dev
terraform workspace select dev
# Créer un workspace pour prod
terraform workspace new prod
terraform workspace select prod
# Utilisation dans le code
resource "local_file" "config" {
filename = "config-${terraform.workspace}.txt"
content = "Environment: ${terraform.workspace}"
}
Le State = carnet de chantier (IMPORTANT)
Terraform note :
- Ce qui existe
- Où c'est
- Comment c'est fait
Si tu supprimes le carnet
❌ Terraform est perdu
➡️ En entreprise : carnet partagé et sécurisé
Backend remote (recommandé)
# backend.tf
terraform {
backend "s3" {
bucket = "mon-terraform-state"
key = "terraform.tfstate"
region = "eu-west-1"
}
}
Alternatives :
- S3 (AWS)
- Azure Storage (Azure)
- GCS (Google Cloud)
- Terraform Cloud (HashiCorp)
Exemple complet : Infrastructure locale
1. Créer le projet
mkdir terraform-exemple
cd terraform-exemple
2. Créer main.tf
terraform {
required_version = ">= 1.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}
# Variable
variable "server_name" {
description = "Nom du serveur"
type = string
default = "web-server"
}
# Ressource
resource "local_file" "server_config" {
filename = "${var.server_name}.txt"
content = <<-EOT
Server Name: ${var.server_name}
Port: 80
Status: Running
EOT
}
# Output
output "server_file" {
value = local_file.server_config.filename
description = "Fichier de configuration créé"
}
3. Initialiser Terraform
terraform init
Cette commande :
- Télécharge les providers nécessaires
- Prépare le backend
- Crée le dossier
.terraform/
4. Voir ce qui va être créé
terraform plan
Résultat :
Terraform will perform the following actions:
# local_file.server_config will be created
+ resource "local_file" "server_config" {
+ content = "Server Name: web-server
Port: 80
Status: Running
"
+ filename = "web-server.txt"
}
Plan: 1 to add, 0 to change, 0 to destroy.
5. Appliquer les changements
terraform apply
Terraform demande confirmation, puis crée le fichier.
6. Vérifier
cat web-server.txt
7. Modifier et réappliquer
Modifiez main.tf :
resource "local_file" "server_config" {
filename = "${var.server_name}.txt"
content = <<-EOT
Server Name: ${var.server_name}
Port: 8080 # Changé de 80 à 8080
Status: Running
EOT
}
terraform plan # Voir les changements
terraform apply # Appliquer
8. Détruire l'infrastructure
terraform destroy
Cas d'usage réel : Déployer Laravel avec Docker sur Debian
Scénario
Tu as déjà un serveur Debian avec Docker installé et qui tourne. Tu veux déployer ton projet Laravel dessus en utilisant Terraform pour gérer toute la configuration.
Prérequis
- Serveur Debian avec Docker et Docker Compose installés
- Accès SSH au serveur
- Terraform installé sur ta machine locale
Structure du projet
terraform-laravel/
├── provider.tf
├── main.tf
├── variables.tf
├── outputs.tf
└── docker-compose.tf
Configuration complète
1. provider.tf - Configuration des providers
terraform {
required_version = ">= 1.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}
# Provider Docker (se connecte au daemon Docker du serveur)
provider "docker" {
host = "ssh://${var.server_user}@${var.server_ip}:${var.server_ssh_port}"
}
2. variables.tf - Variables du projet
variable "server_ip" {
description = "Adresse IP du serveur Debian"
type = string
}
variable "server_user" {
description = "Utilisateur SSH pour se connecter au serveur"
type = string
}
variable "server_ssh_port" {
description = "Port SSH du serveur Debian"
type = number
}
variable "app_name" {
description = "Nom de l'application"
type = string
}
variable "app_port" {
description = "Port sur lequel l'application sera accessible"
type = number
}
variable "db_password" {
description = "Mot de passe root de la base de données MySQL"
type = string
sensitive = true
}
variable "db_name" {
description = "Nom de la base de données"
type = string
}
variable "docker_image" {
description = "Image Docker à utiliser pour l'application Laravel"
type = string
}
# VARIABLES AJOUTEES
variable "git_repo" {
description = "URL du repo Git"
type = string
}
variable "github_token" {
description = "Token GitHub pour accéder aux dépôts privés"
type = string
sensitive = true
}
variable "git_branch" {
description = "Branche Git à cloner"
type = string
default = "main"
}
variable "app_type" {
description = "Type d'application (laravel / generic)"
type = string
default = "laravel"
}
variable "run_migrations" {
description = "Exécuter les migrations Laravel"
type = bool
default = true
}
variable "run_seeders" {
description = "Exécuter les seeders Laravel"
type = bool
default = false
}
3. docker-compose.tf - Créer le fichier docker-compose.yml
# Création du docker-compose.yml
resource "local_file" "docker_compose" {
filename = "docker-compose.yml"
content = <<-EOT
version: '3.8'
services:
app:
image: ${var.docker_image}
container_name: ${var.app_name}-app
ports:
- "${var.app_port}:80"
environment:
GIT_REPO: "${var.git_repo}"
GITHUB_TOKEN: "${var.github_token}"
GIT_BRANCH: "${var.git_branch}"
APP_TYPE: "${var.app_type}"
RUN_MIGRATIONS: "${var.run_migrations}"
RUN_SEEDERS: "${var.run_seeders}"
APP_NAME: "${var.app_name}"
APP_ENV: "dev"
APP_DEBUG: "true"
APP_URL: "http://${var.server_ip}:${var.app_port}"
DB_CONNECTION: "mysql"
DB_HOST: "mysql"
DB_PORT: 3306
DB_DATABASE: "${var.db_name}"
DB_USERNAME: "root"
DB_PASSWORD: "${var.db_password}"
networks:
- laravel-network
depends_on:
- mysql
restart: unless-stopped
mysql:
image: mysql:8.0
container_name: ${var.app_name}-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${var.db_password}"
MYSQL_DATABASE: "${var.db_name}"
volumes:
- mysql_data:/var/lib/mysql
networks:
- laravel-network
ports:
- "3307:3306" # Port externe pour accès direct si besoin
volumes:
mysql_data:
networks:
laravel-network:
driver: bridge
EOT
}
4. main.tf - Déployer les conteneurs
resource "null_resource" "prepare_remote_dir" {
triggers = {
app_name = var.app_name
server_user = var.server_user
server_ip = var.server_ip
server_ssh_port = var.server_ssh_port
}
provisioner "remote-exec" {
inline = [
"mkdir -p /home/${var.server_user}/${var.app_name}"
]
connection {
type = "ssh"
host = var.server_ip
user = var.server_user
port = var.server_ssh_port
private_key = file("~/.ssh/id_eisi25_docker")
}
}
provisioner "remote-exec" {
when = destroy
inline = [
"rm -rf /home/${self.triggers.server_user}/${self.triggers.app_name}"
]
connection {
type = "ssh"
host = self.triggers.server_ip
user = self.triggers.server_user
port = self.triggers.server_ssh_port
private_key = file("~/.ssh/id_eisi25_docker")
}
}
}
# Exécuter docker-compose up sur le serveur
resource "null_resource" "deploy_laravel" {
depends_on = [null_resource.prepare_remote_dir]
triggers = {
docker_compose = local_file.docker_compose.content
app_port = var.app_port
db_password = var.db_password
app_name = var.app_name
server_user = var.server_user
server_ip = var.server_ip
server_ssh_port = var.server_ssh_port
}
# Copier le fichier docker-compose.yml sur le serveur
provisioner "file" {
source = "docker-compose.yml"
destination = "/home/${var.server_user}/${var.app_name}/docker-compose.yml"
connection {
type = "ssh"
host = var.server_ip
user = var.server_user
port = var.server_ssh_port
private_key = file("~/.ssh/id_eisi25_docker")
}
}
# Exécuter docker-compose
provisioner "remote-exec" {
inline = [
"cd /home/${var.server_user}/${var.app_name}",
"docker compose down || true", # Arrêter si déjà en cours
"docker compose pull", # Télécharger la dernière version de l'image
"docker compose up -d", # Démarrer en arrière-plan
"sleep 10", # Attendre que les conteneurs démarrent et que l'entrypoint.sh fasse son travail
"docker compose ps", # Vérifier l'état des conteneurs
"docker compose logs app | tail -20" # Voir les derniers logs pour vérifier
]
connection {
type = "ssh"
host = var.server_ip
user = var.server_user
port = var.server_ssh_port
private_key = file("~/.ssh/id_eisi25_docker")
}
}
provisioner "remote-exec" {
when = destroy
inline = [
"cd /home/${self.triggers.server_user}/${self.triggers.app_name} || exit 0",
"docker compose down -v || true",
"docker compose rm -f || true"
]
connection {
type = "ssh"
host = self.triggers.server_ip
user = self.triggers.server_user
port = self.triggers.server_ssh_port
private_key = file("~/.ssh/id_eisi25_docker")
}
}
}
Note : L'image Docker personnalisée (
ghcr.io/nouvy/laravel-app:latest) contient déjà unentrypoint.shqui :
- Clone le dépôt Git (via
GIT_REPO,GITHUB_TOKEN,GIT_BRANCH)- Configure Laravel (permissions, dépendances Composer)
- Crée le fichier
.envavec les variables d'environnement passées- Génère la clé d'application Laravel
- Lance les migrations si
RUN_MIGRATIONS=true- Lance les seeders si
RUN_SEEDERS=true- Démarre Apache
Les variables d'environnement passées dans
docker-compose.yml(commeDB_HOST,DB_PASSWORD,GIT_REPO, etc.) seront utilisées par l'application Laravel et l'entrypoint.sh.
#### 5. `outputs.tf` - Informations de sortie
```hcl
output "app_url" {
value = "http://${var.server_ip}:${var.app_port}"
description = "URL d'accès à l'application Laravel"
}
output "docker_compose_file" {
value = local_file.docker_compose.filename
description = "Fichier docker-compose.yml créé"
}
6. terraform.tfvars - Valeurs des variables
server_ip = "192.168.194.211"
server_user = "fabrice"
server_ssh_port = 30625
git_repo = "https://github.com/nouvy/laravel-app.git"
github_token = "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"
app_name = "app-laravel-eisi25"
app_port = 8084
db_password = "secret"
db_name = "app-laravel-eisi25"
docker_image = "ghcr.io/nouvy/laravel-app:latest"
Commandes pour déployer
# 1. Initialiser Terraform
terraform init
# 2. Voir ce qui va être créé
terraform plan
# 3. Déployer l'application
terraform apply
# 4. Vérifier l'état
terraform show
Accéder à l'application
Une fois le déploiement terminé, accède à ton application Laravel via :
http://192.168.1.100:8080
Modifier et redéployer
Si tu modifies les variables (par exemple le port) :
# terraform.tfvars
app_port = 9090
terraform plan # Voir les changements
terraform apply # Appliquer
Terraform va automatiquement :
- Mettre à jour le fichier docker-compose.yml
- Redémarrer les conteneurs avec la nouvelle configuration
Détruire l'infrastructure
terraform destroy
Cela va arrêter et supprimer tous les conteneurs Docker.
💡 Pourquoi c'est puissant ?
- ✅ Tout est versionné : Ton infrastructure Laravel est dans Git
- ✅ Reproductible : N'importe qui peut déployer exactement pareil
- ✅ Modifiable facilement : Change une variable, réapplique
- ✅ Documentation vivante : Le code Terraform documente ton infrastructure
Terraform + Ansible
🧩 Rôle de chacun
| Outil | Rôle principal |
|---|---|
| Terraform | Crée l'infrastructure (serveurs, réseaux, cloud) |
| Ansible | Configure ce qui est sur les serveurs (logiciels, fichiers, services) |
💡 Exemple concret
- Terraform crée une machine virtuelle
- Ansible installe dessus Nginx, MySQL, utilisateurs, certificats, etc.
Workflow type
terraform apply→ La machine existe- Ansible prend la machine → Configure le logiciel
- Tout est prêt, versionnable et réutilisable
C'est comme : Construire une maison (Terraform) → La meubler et la décorer (Ansible)
Terraform + Kubernetes
🧩 Rôle de chacun
| Outil | Rôle principal |
|---|---|
| Terraform | Crée les ressources cloud : VM, load balancer, réseau, clusters Kubernetes |
| Kubernetes | Gère les applications à l'intérieur : pods, services, déploiements |
| Helm (optionnel) | Facilite les déploiements Kubernetes comme un gestionnaire de paquets |
💡 Exemple concret
- Terraform crée un cluster Kubernetes complet sur AWS/GCP/Azure
- Kubernetes déploie les applications en microservices, gère la scalabilité et la disponibilité
Workflow type
- Terraform → "Cluster Kubernetes prêt"
- Kubernetes → Déploie toutes les apps
- Helm / YAML → Déploie microservices, base de données, front, etc.
Analogie maison complète
- Terraform → Fondations et murs
- Ansible → Plomberie, électricité, cuisine
- Kubernetes → Locataires et gestion des appartements
Concepts avancés
Data Sources
Permet de récupérer des informations existantes :
data "aws_ami" "latest_amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.latest_amazon_linux.id
instance_type = "t2.micro"
}
Locals
Variables calculées réutilisables :
locals {
common_tags = {
Environment = var.environment
Project = "MonProjet"
ManagedBy = "Terraform"
}
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = local.common_tags
}
Conditions
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.environment == "prod" ? "t3.medium" : "t2.micro"
count = var.create_instance ? 1 : 0
}
For Each
Créer plusieurs ressources similaires :
variable "servers" {
type = map(object({
instance_type = string
}))
default = {
web1 = { instance_type = "t2.micro" }
web2 = { instance_type = "t2.micro" }
db = { instance_type = "t3.small" }
}
}
resource "aws_instance" "servers" {
for_each = var.servers
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value.instance_type
tags = {
Name = each.key
}
}
Bonnes pratiques
1. Versionner le code
git init
git add .
git commit -m "Initial Terraform configuration"
2. Utiliser des modules
Au lieu de tout mettre dans main.tf, créez des modules :
modules/
├── network/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── database/
├── main.tf
├── variables.tf
└── outputs.tf
3. Backend remote pour le state
Ne jamais commiter terraform.tfstate dans Git. Utilisez un backend remote.
4. Utiliser des variables
Ne jamais hardcoder les valeurs. Utilisez des variables et des fichiers .tfvars.
5. Documentation
Ajoutez des descriptions à toutes les variables et outputs :
variable "instance_type" {
description = "Type d'instance EC2 à utiliser"
type = string
default = "t2.micro"
}
6. Validation des variables
variable "environment" {
description = "Environnement de déploiement"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être dev, staging ou prod."
}
}
Erreurs courantes et solutions
Erreur : Provider not found
Error: Failed to query available provider packages
Solution : Exécutez terraform init
Erreur : State locked
Error: Error acquiring the state lock
Solution : Un autre processus utilise Terraform. Attendez ou forcez le déverrouillage :
terraform force-unlock <LOCK_ID>
Erreur : Resource already exists
Solution : Importez la ressource existante :
terraform import aws_instance.web i-1234567890abcdef0
Commandes essentielles
| Commande | Description |
|---|---|
terraform init |
Initialise le projet et télécharge les providers |
terraform plan |
Affiche ce qui va changer (sans appliquer) |
terraform apply |
Applique les changements |
terraform destroy |
Détruit toutes les ressources |
terraform validate |
Valide la syntaxe des fichiers |
terraform fmt |
Formate les fichiers .tf |
terraform show |
Affiche l'état actuel |
terraform output |
Affiche les outputs |
terraform state list |
Liste toutes les ressources dans le state |
terraform state show <resource> |
Affiche les détails d'une ressource |
Résumé en 5 phrases
- Terraform décrit l'état final voulu → Tu écris ce que tu veux, pas comment
- Tu peux tout refaire exactement pareil → Infrastructure reproductible
- Les modules sont des plans → Code réutilisable et organisé
- Les variables adaptent le résultat → Même code, environnements différents
- Le state est la mémoire de Terraform → Ne jamais le perdre, utiliser un backend remote
Ressources supplémentaires
- Documentation officielle Terraform
- Terraform Registry - Modules et providers
- Terraform Best Practices
- Terraform AWS Provider
