07 — Terraform avec les providers cloud#
Terraform tire toute sa puissance de ses providers : chaque fournisseur cloud expose des centaines de ressources gérables via HCL. Ce chapitre couvre les trois grands clouds (AWS, GCP, Azure), les patterns d’infrastructure moderne, la stratégie multi-cloud, Terragrunt et Pulumi comme alternatives complémentaires.
Les trois grands clouds — équivalences de services#
Domaine |
AWS |
GCP |
Azure |
|---|---|---|---|
Réseau virtuel |
VPC |
VPC (Global) |
VNet |
Machine virtuelle |
EC2 |
Compute Engine (GCE) |
Virtual Machine |
Kubernetes managé |
EKS |
GKE |
AKS |
Base de données relationnelle |
RDS |
Cloud SQL |
Azure SQL |
Stockage objet |
S3 |
GCS |
Blob Storage |
Identité & accès |
IAM |
IAM |
Azure AD / RBAC |
DNS |
Route 53 |
Cloud DNS |
Azure DNS |
Fonctions serverless |
Lambda |
Cloud Functions |
Azure Functions |
Registre de conteneurs |
ECR |
Artifact Registry |
Azure Container Registry |
Secrets |
Secrets Manager |
Secret Manager |
Key Vault |
Provider AWS#
Configuration du provider#
# versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/TerraformDeployRole"
}
default_tags {
tags = local.common_tags
}
}
VPC et réseau#
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "${local.name_prefix}-vpc" }
}
resource "aws_subnet" "private" {
for_each = {
"eu-west-1a" = "10.0.1.0/24"
"eu-west-1b" = "10.0.2.0/24"
"eu-west-1c" = "10.0.3.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
tags = { Name = "private-${each.key}" }
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public["eu-west-1a"].id
depends_on = [aws_internet_gateway.main]
}
EKS#
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "${local.name_prefix}-eks"
cluster_version = "1.29"
vpc_id = aws_vpc.main.id
subnet_ids = [for s in aws_subnet.private : s.id]
cluster_endpoint_public_access = false
cluster_endpoint_private_access = true
eks_managed_node_groups = {
system = {
instance_types = ["t3.medium"]
min_size = 2
max_size = 4
desired_size = 2
}
app = {
instance_types = ["c5.xlarge"]
min_size = 2
max_size = 20
desired_size = 4
capacity_type = "SPOT"
}
}
}
RDS#
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-postgres"
engine = "postgres"
engine_version = "15.4"
instance_class = var.rds_instance_class
allocated_storage = 100
max_allocated_storage = 1000 # autoscaling storage
db_name = var.db_name
username = var.db_username
password = var.db_password # sensitive
multi_az = local.is_production
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 7
deletion_protection = local.is_production
skip_final_snapshot = !local.is_production
performance_insights_enabled = true
lifecycle {
ignore_changes = [password]
}
}
Provider GCP#
Configuration du provider GCP#
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
provider "google-beta" {
project = var.gcp_project_id
region = var.gcp_region
}
VPC et GKE#
resource "google_compute_network" "main" {
name = "${local.name_prefix}-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "private" {
name = "${local.name_prefix}-subnet-private"
ip_cidr_range = "10.0.1.0/24"
region = var.gcp_region
network = google_compute_network.main.id
secondary_ip_range {
range_name = "pods"
ip_cidr_range = "10.1.0.0/16"
}
secondary_ip_range {
range_name = "services"
ip_cidr_range = "10.2.0.0/20"
}
}
resource "google_container_cluster" "main" {
name = "${local.name_prefix}-gke"
location = var.gcp_region
# Cluster autopilot (recommandé)
enable_autopilot = true
network = google_compute_network.main.id
subnetwork = google_compute_subnetwork.private.id
ip_allocation_policy {
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}
private_cluster_config {
enable_private_nodes = true
enable_private_endpoint = false
master_ipv4_cidr_block = "172.16.0.0/28"
}
}
Provider Azure#
Configuration du provider Azure#
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = true
}
key_vault {
purge_soft_delete_on_destroy = false
}
}
subscription_id = var.azure_subscription_id
}
VNet, AKS et Azure SQL#
resource "azurerm_resource_group" "main" {
name = "${local.name_prefix}-rg"
location = var.azure_location
tags = local.common_tags
}
resource "azurerm_virtual_network" "main" {
name = "${local.name_prefix}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_kubernetes_cluster" "main" {
name = "${local.name_prefix}-aks"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
dns_prefix = local.name_prefix
kubernetes_version = "1.29"
default_node_pool {
name = "system"
node_count = 2
vm_size = "Standard_D4s_v5"
vnet_subnet_id = azurerm_subnet.aks.id
enable_auto_scaling = true
min_count = 2
max_count = 10
}
identity {
type = "SystemAssigned"
}
network_profile {
network_plugin = "azure"
load_balancer_sku = "standard"
}
}
resource "azurerm_mssql_server" "main" {
name = "${local.name_prefix}-sqlserver"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = var.db_username
administrator_login_password = var.db_password
}
Infrastructure cloud moderne — pattern de référence#
Une infrastructure cloud moderne réunit typiquement :
Réseau : VPC/VNet avec subnets privés/publics, NAT gateway, peering, VPN ou Direct Connect
Compute : Kubernetes managé (EKS/GKE/AKS) avec node groups hétérogènes (on-demand + spot)
Données : Base relationnelle managée avec réplicas en lecture, cache Redis, stockage objet
DNS et trafic : Load balancer + ingress controller, CDN, certificats TLS automatisés
Sécurité : IAM avec moindre privilège, secrets managés, chiffrement at-rest et in-transit
Observabilité : métriques, logs, traces corrélées (voir chapitres dédiés)
Stratégie multi-cloud#
Terraform permet de gérer plusieurs clouds dans la même configuration grâce à l’instanciation de providers distincts.
# Abstraction cross-cloud : déploiement d'un bucket de logs sur AWS et GCP
module "logs_aws" {
source = "./modules/object-storage/aws"
bucket_name = "logs-${var.environment}"
region = "eu-west-1"
versioning = true
}
module "logs_gcp" {
source = "./modules/object-storage/gcp"
bucket_name = "logs-${var.project}-${var.environment}"
location = "EU"
versioning = true
}
Complexité du multi-cloud
Le multi-cloud augmente la surface de gestion. Terraform abstrait le provisioning mais pas les différences sémantiques entre providers (politiques IAM, modèles réseau, types de nœuds Kubernetes). Justifiez le multi-cloud par des exigences métier réelles (résilience, réglementation) et non par le seul fait d’éviter le vendor lock-in.
Terragrunt : DRY pour Terraform#
Terragrunt est un wrapper Terraform qui résout deux problèmes récurrents : la duplication de configuration backend et la gestion des dépendances entre stacks.
# terragrunt.hcl (à la racine du dépôt)
locals {
account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
account_id = local.account_vars.locals.account_id
environment = local.env_vars.locals.environment
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "tf-states-${local.account_id}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# envs/prod/eks/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-00000000"
private_subnet_ids = ["subnet-00000000"]
}
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
# Déployer toutes les stacks d'un environnement dans l'ordre des dépendances
terragrunt run-all apply --terragrunt-working-dir envs/prod
# Planifier en parallèle
terragrunt run-all plan --terragrunt-parallelism 4
Pulumi : positionnement vs Terraform#
Pulumi prend une approche différente : l’infrastructure est décrite dans un langage de programmation généraliste (TypeScript, Python, Go, Java, C#) plutôt qu’un DSL déclaratif.
# Exemple Pulumi Python — équivalent du module EKS Terraform
import pulumi
import pulumi_aws as aws
import pulumi_eks as eks
# Les ressources sont des objets Python
vpc = aws.ec2.Vpc("main-vpc",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True)
cluster = eks.Cluster("prod-eks",
vpc_id=vpc.id,
instance_type="t3.medium",
desired_capacity=3,
min_size=2,
max_size=10)
pulumi.export("kubeconfig", cluster.kubeconfig)
Critère |
Terraform / OpenTofu |
Pulumi |
|---|---|---|
Langage |
HCL (DSL déclaratif) |
Python, TypeScript, Go, Java… |
Courbe d’apprentissage |
Modérée (HCL spécifique) |
Variable (dépend du langage connu) |
Logique conditionnelle |
Limitée (ternaire, for) |
Complète (boucles, classes, libs) |
State management |
Fichier tfstate / backends |
Pulumi Cloud ou backends S3/GCS |
Écosystème providers |
3000+ providers |
Basé sur les providers Terraform |
Tests unitaires |
Faibles (terratest externe) |
Natifs (pytest, jest…) |
Coûts et optimisation#
# Utiliser des instances Spot/Preemptible pour les workloads tolerant les interruptions
resource "aws_eks_node_group" "spot" {
# ...
capacity_type = "SPOT"
instance_types = ["c5.xlarge", "c5a.xlarge", "c5d.xlarge"] # plusieurs types pour disponibilité
scaling_config {
desired_size = 4
min_size = 2
max_size = 20
}
}
# Rightsizing : utiliser des locals pour centraliser les tailles par environnement
locals {
instance_sizes = {
dev = { db = "db.t3.micro", eks = "t3.small" }
staging = { db = "db.t3.medium", eks = "t3.medium" }
prod = { db = "db.r6g.large", eks = "c5.xlarge" }
}
sizes = local.instance_sizes[var.environment]
}
Infracost — estimation de coût dans la CI
L’outil Infracost s’intègre dans le pipeline CI et affiche l’impact financier de chaque terraform plan avant l’apply. Il permet de détecter les augmentations de coût involontaires (ex : changement de type d’instance, ajout d’un NAT gateway).
Visualisations#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Visualisation 1 : Radar chart comparatif AWS / GCP / Azure
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
categories = ["Kubernetes\nmanagé", "Base de\ndonnées", "Serverless", "Prix relatif\n(inversé)", "Maturité\nIaC", "Écosystème\noutils"]
N = len(categories)
# Scores sur 10 (subjectifs, à des fins pédagogiques)
scores = {
"AWS": [9, 9, 9, 6, 9, 9],
"GCP": [9, 8, 8, 7, 8, 7],
"Azure": [8, 8, 7, 7, 8, 8],
}
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles += angles[:1]
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, size=10)
ax.set_ylim(0, 10)
ax.set_yticks([2, 4, 6, 8, 10])
ax.set_yticklabels(["2", "4", "6", "8", "10"], size=8, color="#868e96")
ax.set_title("Comparaison AWS / GCP / Azure\n(services cloud pour infrastructure moderne)", fontsize=13, fontweight="bold", pad=25)
colors_radar = {"AWS": "#ff922b", "GCP": "#4dabf7", "Azure": "#74c0fc"}
for provider, vals in scores.items():
vals_closed = vals + vals[:1]
ax.plot(angles, vals_closed, linewidth=2.2, label=provider, color=colors_radar[provider])
ax.fill(angles, vals_closed, alpha=0.12, color=colors_radar[provider])
ax.legend(loc="upper right", bbox_to_anchor=(1.35, 1.15), fontsize=10)
plt.savefig("cloud_radar.png", dpi=120, bbox_inches="tight")
plt.show()
# Visualisation 2 : Simulation de coût selon la stratégie multi-régions
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
strategies = ["Single region", "Active-passive\n(2 régions)", "Active-active\n(3 régions)"]
composants = ["Compute (EKS)", "Base de données", "Réseau (data transfer)", "Load balancers", "Stockage"]
# Coûts mensuels simulés en €
costs = np.array([
[1200, 2400, 3800], # Compute
[ 400, 800, 1200], # BDD
[ 100, 400, 900], # Réseau
[ 150, 350, 600], # LB
[ 200, 400, 700], # Stockage
])
x = np.arange(len(strategies))
width = 0.14
palette = ["#4dabf7", "#51cf66", "#ff922b", "#cc5de8", "#ffd43b"]
fig, ax = plt.subplots(figsize=(11, 6))
bottoms = np.zeros(len(strategies))
for i, (comp, color) in enumerate(zip(composants, palette)):
bars = ax.bar(x, costs[i], width=0.55, bottom=bottoms,
label=comp, color=color, alpha=0.88, edgecolor="white", linewidth=0.8)
bottoms += costs[i]
# Totaux
for i, total in enumerate(bottoms):
ax.text(i, total + 40, f"{total:,.0f} €/mois", ha="center", va="bottom",
fontsize=10, fontweight="bold", color="#333")
ax.set_xticks(x)
ax.set_xticklabels(strategies, fontsize=11)
ax.set_ylabel("Coût mensuel estimé (€)")
ax.set_title("Simulation de coût d'infrastructure multi-régions\nselon la stratégie de déploiement", fontsize=13, fontweight="bold")
ax.legend(title="Composant", loc="upper left", fontsize=9)
ax.set_ylim(0, 8500)
plt.savefig("multiregion_costs.png", dpi=120, bbox_inches="tight")
plt.show()
# Visualisation 3 : Heatmap comparative Terraform vs Pulumi vs CDK
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
criteres = [
"Flexibilité du\nlangage",
"Courbe d'appren-\ntissage IaC",
"Écosystème\nproviders",
"Gestion\ndu state",
"Tests\nunitaires",
"Maturité /\ncommunauté",
"Multi-cloud\nnatif",
"Intégration\nCI/CD",
]
# Scores /10 pour chaque outil
scores_matrix = np.array([
# Terraform Pulumi CDK (AWS)
[4, 9, 7 ], # Flexibilité langage
[7, 6, 5 ], # Courbe apprentissage (élevé = facile)
[9, 8, 5 ], # Écosystème providers
[8, 7, 6 ], # Gestion state
[4, 9, 8 ], # Tests unitaires
[9, 7, 7 ], # Maturité
[9, 8, 3 ], # Multi-cloud
[9, 8, 7 ], # CI/CD
])
fig, ax = plt.subplots(figsize=(9, 8))
im = ax.imshow(scores_matrix, cmap="YlGn", aspect="auto", vmin=0, vmax=10)
outils = ["Terraform /\nOpenTofu", "Pulumi", "AWS CDK"]
ax.set_xticks(range(len(outils)))
ax.set_yticks(range(len(criteres)))
ax.set_xticklabels(outils, fontsize=11, fontweight="bold")
ax.set_yticklabels(criteres, fontsize=9.5)
ax.set_title("Comparaison Terraform / Pulumi / AWS CDK\n(score /10)", fontsize=13, fontweight="bold", pad=15)
for i in range(len(criteres)):
for j in range(len(outils)):
val = scores_matrix[i, j]
color = "white" if val > 7 else "#333"
ax.text(j, i, str(val), ha="center", va="center", fontsize=12, fontweight="bold", color=color)
plt.colorbar(im, ax=ax, label="Score (10 = meilleur)", shrink=0.8)
plt.savefig("iac_tools_heatmap.png", dpi=120, bbox_inches="tight")
plt.show()
Résumé#
Les trois grands clouds partagent les mêmes catégories de services (réseau, compute, K8s managé, BDD, IAM) mais avec des modèles et des APIs suffisamment différents pour que chaque provider Terraform soit distinct.
Le provider AWS est le plus mature et le plus riche en ressources gérées par Terraform ; les modules
terraform-aws-modulescouvrent l’essentiel des besoins courants.Le provider GCP se distingue par son réseau global (pas de régional par défaut) et GKE Autopilot, qui délègue entièrement la gestion des nœuds à Google.
Le provider Azure impose la notion de
resource_groupcomme conteneur de toutes les ressources, ce qui structure naturellement le découpage par domaine ou environnement.Une infrastructure cloud moderne combine réseau privé, Kubernetes managé pour le compute applicatif, base de données managée, et une couche IAM fine — tous ces éléments s’orchestrent naturellement avec Terraform.
Le multi-cloud avec Terraform est techniquement possible mais doit être justifié par des exigences métier réelles ; la complexité opérationnelle augmente significativement.
Terragrunt résout les limitations de Terraform en matière de DRY pour la configuration backend et les dépendances inter-stacks, sans imposer un nouveau langage.
Pulumi offre la puissance d’un vrai langage de programmation (boucles, classes, tests) au prix d’une courbe d’apprentissage plus longue et d’un écosystème moins étendu que Terraform.
L’optimisation des coûts doit être intégrée dès la conception IaC : instances Spot pour les workloads résilients, rightsizing par environnement via des locals, et Infracost dans la CI.
Quel que soit le cloud ou l’outil, les principes restent constants : modules réutilisables, state distant verrouillé, pipeline CI/CD pour chaque changement, et révision de code systématique pour l’infrastructure.