Introduction
Terraform, l'outil open-source d'HashiCorp pour l'Infrastructure as Code (IaC), reste en 2026 le standard pour orchestrer des environnements complexes et multi-cloud. Contrairement aux scripts manuels, Terraform applique une approche déclarative : vous décrivez l'état désiré dans des fichiers HCL, et il gère les diffs via plan et apply.
Pourquoi ce tutoriel avancé ? Les débutants se contentent d'un main.tf basique, mais en prod, vous gérez des modules réutilisables, des states distants pour la collaboration, des workspaces pour dev/staging/prod, et des providers multiples (AWS + GCP ici). Nous couvrons ces patterns avec du code 100% fonctionnel sur une infra VPC/EC2 (AWS) + VPC/VM (GCP), incluant data sources, for_each, et remote backend S3.
À la fin, vous bookmarkederez ce guide pour vos déploiements scalables : zéro downtime, audits traçables, et conformité cloud-native. Temps estimé : 45 min pour un setup complet (128 mots).
Prérequis
- Terraform CLI v1.9+ installé (
terraform version) - Comptes AWS et GCP actifs avec IAM/Service Account (clés API)
- AWS CLI configuré + bucket S3 pour state distant
- GCP CLI (
gcloud auth login) - Éditeur VS Code avec extension HashiCorp Terraform
- Connaissances IaC intermédiaires (providers, resources)
Initialisation du projet et providers
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "multi-cloud/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
}
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}Ce fichier initialise Terraform avec providers AWS/GCP pinned en version, et un backend S3 pour state distant (avec DynamoDB pour locks). Remplacez your-terraform-state-bucket par votre bucket. Piège : Oublier encrypt=true expose les states ; utilisez terraform init après pour migrer local vers remote.
Définition des variables d'entrée
Avant les ressources, définissons des variables réutilisables pour les environnements. Cela permet de passer dev, prod via CLI ou TF_VARs, évitant les hardcodings.
Variables et outputs
variable "aws_region" {
description = "Région AWS"
type = string
default = "eu-west-1"
}
variable "gcp_project_id" {
description = "ID projet GCP"
type = string
}
variable "gcp_region" {
description = "Région GCP"
type = string
default = "europe-west1"
}
variable "environment" {
description = "Environnement (dev/prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environnement doit être dev, staging ou prod."
}
}
output "aws_vpc_id" {
description = "ID VPC AWS créée"
value = module.aws_vpc.vpc_id
}
output "gcp_network_name" {
description = "Nom réseau GCP"
value = module.gcp_vpc.network_name
}Variables avec validation bloquent les inputs invalides (ex: terraform plan -var='environment=foo' échoue). Outputs exposent les IDs pour chaining. Avancé : Utilisez default pour dev, mais forcez via -var en prod pour sécurité.
Création d'un module VPC AWS réutilisable
Passons aux modules : encapsulez la logique VPC en sous-dossier pour réutilisation. Ici, un module AWS crée VPC + subnets avec for_each pour haute disponibilité.
Module AWS VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "vpc-${var.environment}
Environment = var.environment
}
}
resource "aws_subnet" "public" {
for_each = toset(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
map_public_ip_on_launch = true
tags = {
Name = "public-${each.key}-${var.environment}
Environment = var.environment
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "igw-${var.environment}
}
}for_each itère sur AZs/CIDRs dynamiquement (clé=valeur), scalable vs count. Ajoutez variables.tf dans module : variable "public_subnet_cidrs" { type = map(string) }. Appel : module "aws_vpc" { source = "./modules/aws_vpc" ... }. Piège : CIDRs overlap causent erreurs plan.
Appel module AWS et instance EC2
module "aws_vpc" {
source = "./modules/aws_vpc"
environment = var.environment
public_subnet_cidrs = {
"a" = "10.0.1.0/24"
"b" = "10.0.2.0/24"
"c" = "10.0.3.0/24"
}
}
resource "aws_instance" "web" {
for_each = module.aws_vpc.public_subnet_ids
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = each.value
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "web-${each.key}-${var.environment}
Environment = var.environment
}
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
resource "aws_security_group" "web" {
vpc_id = module.aws_vpc.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg-${var.environment}
}
}Data source fetch AMI dynamique (évite hardcode). for_each sur subnets module output (ajoutez output "public_subnet_ids" { value = {for k,v in aws_subnet.public : k => v.id} }). SG basique pour HTTP. Piège : Oublier data source = AMI introuvable.
Module GCP équivalent et data sources
Symétrique pour GCP : module VPC + VM. Utilisez data sources pour fetch zones existantes, rendant le module portable.
Module GCP VPC et appel
module "gcp_vpc" {
source = "./modules/gcp_vpc"
environment = var.environment
project_id = var.gcp_project_id
}
resource "google_compute_instance" "web" {
for_each = data.google_compute_zones.available.names
name = "web-${each.key}-${var.environment}
machine_type = "e2-micro"
zone = each.value
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = module.gcp_vpc.network_name
access_config {}
}
tags = ["http-server", "${var.environment}"]
}Data google_compute_zones (définie plus bas) pour HA multi-zone. Instance Debian avec tag firewall. Créez modules/gcp_vpc/main.tf similaire : VPC 10.0.0.0/16, subnet, output network_name. Piège : Tags manquants = firewall KO.
Data sources GCP et script init
data "google_compute_zones" "available" {
region = var.gcp_region
status = "UP"
}
resource "google_compute_firewall" "web" {
name = "allow-http-${var.environment}
network = module.gcp_vpc.network_name
allow {
protocol = "tcp"
ports = ["80"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["http-server"]
}Data zones filtrées UP pour résilience. Firewall par tags. Ajoutez à modules/gcp_vpc/outputs.tf : output "network_name" { value = google_compute_network.main.name }. Fonctionnel : terraform apply expose HTTP.
Workspaces et apply bash
#!/bin/bash
ENV=${1:-dev}
terraform workspace new ${ENV} || terraform workspace select ${ENV}
terraform init \
-backend-config="bucket=your-terraform-state-bucket" \
-backend-config="key=multi-cloud/${ENV}/terraform.tfstate" \
-backend-config="region=eu-west-1"
terraform plan -var="environment=${ENV}" \
-var="gcp_project_id=your-gcp-project"
terraform apply -auto-approve -var="environment=${ENV}" \
-var="gcp_project_id=your-gcp-project"
terraform output -json > outputs-${ENV}.json
echo "Infra ${ENV} déployée ! Outputs dans outputs-${ENV}.json"Script bash pour workspaces par env (isole states). backend-config override key par env. Lancez ./deploy.sh prod. Piège : Sans -auto-approve, review manuelle ; toujours plan avant.
Bonnes pratiques
- Modularisez tout : Un
main.tf< 300 lignes ; source Git pour modules privés. - State remote obligatoire : S3 + DynamoDB en team ;
terraform state mvpour migrations. - Variables validées + secrets : Utilisez
sensitive=true, Terraform Cloud Vault ou AWS SSM. - Plan en CI/CD : GitHub Actions avec
terraform plan -out=tfplan+applysur merge. - Destroy safe :
terraform destroy -target=module.xxxpour rollback granulaire.
Erreurs courantes à éviter
- State drift : Ne modifiez jamais manuellement en console cloud ;
terraform refreshavant plan. - Provider version drift : Pinnez
~> 5.0;terraform providers lockgénère.terraform.lock.hcl. - Cycles de dépendances : Utilisez
depends_onexplicitement siimplicitéchoue. - Workspaces oubliés :
terraform workspace list; delete avecrm -rf .terraformsi corrompu.
Pour aller plus loin
- Docs officielles : Terraform Registry
- Avancé : Terragrunt pour DRY configs, Atlantis pour GitOps.
- Formations pro : Learni DevOps & IaC
- Exemple GitHub : Forkez ce repo pour customiser.