Le VPC (Virtual Private Cloud) constitue la brique fondamentale de toute architecture AWS. Avant de déployer la moindre ressource, il faut définir le réseau dans lequel elle va vivre. Les ingénieurs cloud utilisent aujourd’hui de plus en plus l’Infrastructure as Code (IaC) pour provisionner ces ressources de façon reproductible et versionnée — et Terraform s’est imposé comme l’outil de référence.

Dans cet article, nous allons créer pas à pas un VPC complet sur AWS avec Terraform.

Architecture cible

Nous allons construire un VPC avec des sous-réseaux publics et privés répartis sur deux zones de disponibilité (AZ) pour assurer la haute disponibilité.

  • Sous-réseaux publics : hébergent les serveurs accessibles depuis Internet (serveurs web, bastions).
  • Sous-réseaux privés : hébergent les serveurs backend et les bases de données.
  • Une NAT Gateway permet aux ressources privées d’accéder à Internet en sortie, sans être exposées en entrée.

Architecture VPC — Region AWS avec VPC, subnets publics et privés répartis sur deux Availability Zones

Ressources à créer :

  • VPC avec CIDR 10.0.0.0/16
  • 2 sous-réseaux publics (un par AZ) avec attribution d’IP publique automatique
  • 2 sous-réseaux privés (un par AZ)
  • Internet Gateway pour la connectivité Internet des subnets publics
  • Table de routage publique pointant vers l’IGW
  • NAT Gateway dans un subnet public
  • Table de routage privée pointant vers la NAT Gateway

Organisation des fichiers Terraform

Un projet Terraform bien structuré sépare les responsabilités :

Fichier Rôle
provider.tf Version de Terraform et du provider AWS
backend.tf (optionnel) Configuration du state file (S3 recommandé)
main.tf Déclaration de toutes les ressources
variables.tf Déclaration des variables
terraform.tfvars Valeurs des variables
outputs.tf Valeurs affichées après le déploiement

Le code Terraform

provider.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variables.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
variable "aws_region" {
  description = "Région AWS"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR du VPC"
  type        = string
}

variable "public_subnets" {
  description = "Liste des CIDR des subnets publics"
  type        = list(string)
}

variable "private_subnets" {
  description = "Liste des CIDR des subnets privés"
  type        = list(string)
}

variable "availability_zones" {
  description = "AZs utilisées"
  type        = list(string)
}

terraform.tfvars

1
2
3
4
5
aws_region         = "eu-west-1"
vpc_cidr           = "10.0.0.0/16"
public_subnets     = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets    = ["10.0.3.0/24", "10.0.4.0/24"]
availability_zones = ["eu-west-1a", "eu-west-1b"]

main.tf

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name      = "demo-vpc"
    terraform = "true"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name      = "demo-igw"
    terraform = "true"
  }
}

# Subnets publics
resource "aws_subnet" "public" {
  for_each = toset(var.public_subnets)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value
  availability_zone       = element(var.availability_zones, index(var.public_subnets, each.value))
  map_public_ip_on_launch = true

  tags = {
    Name      = "public-${each.value}"
    terraform = "true"
  }
}

# Subnets privés
resource "aws_subnet" "private" {
  for_each = toset(var.private_subnets)

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = element(var.availability_zones, index(var.private_subnets, each.value))

  tags = {
    Name      = "private-${each.value}"
    terraform = "true"
  }
}

# Elastic IP pour la NAT Gateway
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
    Name      = "demo-nat-eip"
    terraform = "true"
  }
}

# NAT Gateway
resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = element(values(aws_subnet.public)[*].id, 0)

  tags = {
    Name      = "demo-nat"
    terraform = "true"
  }

  depends_on = [aws_internet_gateway.igw]
}

# Table de routage publique
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name      = "public-rt"
    terraform = "true"
  }
}

# Association subnets publics → table de routage publique
resource "aws_route_table_association" "public_assoc" {
  for_each       = aws_subnet.public
  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

# Table de routage privée
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }

  tags = {
    Name      = "private-rt"
    terraform = "true"
  }
}

# Association subnets privés → table de routage privée
resource "aws_route_table_association" "private_assoc" {
  for_each       = aws_subnet.private
  subnet_id      = each.value.id
  route_table_id = aws_route_table.private.id
}

outputs.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
output "vpc_id" {
  description = "ID du VPC"
  value       = aws_vpc.main.id
}

output "public_subnets" {
  description = "Subnets publics"
  value       = [for s in aws_subnet.public : s.id]
}

output "private_subnets" {
  description = "Subnets privés"
  value       = [for s in aws_subnet.private : s.id]
}

output "nat_gateway_ip" {
  description = "IP publique de la NAT Gateway"
  value       = aws_eip.nat.public_ip
}

Déploiement

Prérequis

Configurez votre profil AWS :

1
aws configure

Commandes

1
2
3
4
5
6
7
8
# Initialise le projet et télécharge les providers
terraform init

# Affiche le plan d'exécution — vérifiez toujours avant d'appliquer
terraform plan

# Applique les changements
terraform apply --auto-approve

Attention : l’option --auto-approve supprime la confirmation manuelle. Ne l’utilisez que lorsque vous avez validé le plan et que vous êtes certain de ce qui va être créé.

Vérification dans la console AWS

Une fois le terraform apply terminé, rendez-vous dans la console AWS → VPC pour vérifier la création de :

  • Le VPC demo-vpc
  • Les 4 subnets (2 publics, 2 privés)
  • L’Internet Gateway demo-igw
  • La NAT Gateway demo-nat
  • Les deux tables de routage avec leurs associations

Bonnes pratiques : le tagging

Le tagging est souvent négligé mais il est indispensable pour la gestion et la facturation des ressources, surtout dans les grandes organisations.

Appliquez au minimum ces deux tags sur toutes vos ressources :

Tag Valeur Utilité
Name Nom lisible de la ressource Identification dans la console
terraform "true" Identifier les ressources gérées par Terraform

Vous pouvez étendre avec des tags Environment, Project, Owner, CostCenter selon votre organisation.

Conclusion

Vous disposez maintenant d’un VPC complet, multi-AZ, déployé via Terraform et prêt pour accueillir vos workloads AWS. Ce pattern — subnets publics/privés, IGW, NAT Gateway — est la base de toute architecture cloud sérieuse.

Le code source complet est disponible sur GitHub.

Dans un prochain article, nous verrons comment aller plus loin en ajoutant des Security Groups et un bastion host pour sécuriser l’accès à vos ressources privées.