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()
_images/5335cd22ad829edee05dd289159952b59748a14d83db345d12c53f01ccb5cb01.png
# 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()
_images/2157ff740bb57f9bd24ff330d88b2336dc1e9cf1264cff0348e3c3fc83bbf6e0.png
# 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()
_images/e52bf87d466cdc1555f3e9c505c80f96858236966ab3c1fe2165cb06fa8cc314.png

Résumé#

  1. 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.

  2. Le provider AWS est le plus mature et le plus riche en ressources gérées par Terraform ; les modules terraform-aws-modules couvrent l’essentiel des besoins courants.

  3. 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.

  4. Le provider Azure impose la notion de resource_group comme conteneur de toutes les ressources, ce qui structure naturellement le découpage par domaine ou environnement.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.