Aller au contenu
English
English
Infrastructure as Code medium

Terraform AWS, Security groups, subnet et instance

70 min de lecture

logo terraform

Créer une EC2 sans parler du réseau fonctionne pour un premier test, mais ce n'est pas encore une infrastructure exploitable. Très vite, vous devez répondre à trois questions concrètes : dans quel subnet l'instance doit vivre, quel security group filtre le trafic, et quels ports sont réellement ouverts. C'est cette transition entre « ça démarre » et « c'est joignable proprement » que ce guide traite.

Vous allez relire des ressources AWS déjà présentes, puis ajouter votre première couche réseau explicite. L'idée importante est la suivante : vous ne recréez pas tout. Vous branchez votre instance sur un réseau existant et vous lui attachez un pare-feu virtuel qui définit ses règles d'entrée.

  • Lire les ressources existantes avec les data sources (aws_vpc, aws_subnet)
  • Créer un security group pour contrôler les règles réseau
  • Autoriser SSH et HTTP via aws_vpc_security_group_ingress_rule
  • Attacher une instance à un subnet spécifique avec des règles réseau précises
  • Vérifier les sorties : subnet, security group, IP publique/privée

Dans AWS, le réseau n'est pas un détail caché derrière l'instance. Une EC2 hérite d'un contexte réseau précis : VPC, subnet, security group. Si vous ne maîtrisez pas cet enchaînement, vous obtenez facilement une machine qui existe mais que vous ne pouvez ni joindre, ni sécuriser correctement.

Ce guide vous apprend donc une logique qui reviendra partout ensuite : lire ce qui existe, ajouter seulement la couche nécessaire, puis vérifier les identifiants réseau obtenus.

Vous allez construire une infrastructure en quatre temps, dont les deux premiers ne créent rien :

  1. Récupérer le VPC par défaut d'AWS
  2. Sélectionner un subnet existant
  3. Créer un security group avec ses règles d'ingress SSH et HTTP
  4. Lancer une EC2 dans ce subnet, protégée par ce groupe

Durée estimée : 15 minutes Coût : peut être couvert par l'offre gratuite selon la date de création du compte et le plan choisi à l'inscription, quelques centimes sinon

Terraform lit tous les fichiers .tf du répertoire courant et les traite comme une seule configuration : l'ordre des fichiers et des blocs n'a aucune importance. Travaillez donc dans un répertoire vide et dédié, sinon une ancienne configuration oubliée sera fusionnée avec la nouvelle.

Fenêtre de terminal
mkdir -p ~/terraform-aws-sg-subnet
cd ~/terraform-aws-sg-subnet

Ce fichier verrouille les versions avant tout le reste. required_version refuse d'exécuter le code avec un binaire Terraform trop ancien, et la contrainte ~> 5.0 autorise les mises à jour mineures du provider en retenant le passage à la majeure. La région n'est pas codée en dur : elle vient d'une variable déclarée à l'étape suivante.

Créez versions.tf pour configurer le provider et les versions :

terraform {
required_version = ">= 1.11.0"
required_providers {
aws = {
source = "hashicorp/aws"
# Série 5.x assumée. La 6.x est la série courante, et elle ne casse
# pas ce type de configuration : mesuré le 2026-09-22 sur le guide
# « Rôle IAM et instance profile », dont le `terraform validate`
# passe en 6.66.0. Ce guide-ci n'a pas été rejoué, et la contrainte
# ne bougera qu'après un cycle complet sur un compte réel.
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}

Cela dit à Terraform :

  • Utiliser Terraform ≥ 1.11.0
  • Charger le provider AWS version ~5.0 (5.x stable)
  • Se connecter à AWS dans la région définie par var.aws_region

Chaque variable porte une valeur par défaut, donc la configuration s'applique telle quelle sans fichier de valeurs. instance_type est celle qui touche à la facturation : t3.micro reste éligible à l'offre gratuite quelle que soit la date de création du compte, contrairement à t2.micro.

Créez variables.tf :

variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "instance_name" {
description = "Name tag for the EC2 instance"
type = string
default = "lab02-instance-with-sg"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}

Ces variables vous permettront de changer de région ou de type d'instance sans modifier le code principal.

Ces trois blocs ne créent rien. Une data source interroge AWS et rend des valeurs que le reste de la configuration consomme, ce qui évite de coder un identifiant en dur : celui d'une AMI change à chaque région et à chaque publication d'image. most_recent retient la plus récente parmi celles que le filtre accepte, et owners restreint la recherche au compte officiel de Canonical.

Créez main.tf avec les data sources :

# Récupérer le VPC par défaut
data "aws_vpc" "default" {
default = true
}
# Récupérer le premier subnet disponible du VPC par défaut
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
# Lire l'AMI Ubuntu la plus récente
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical (Ubuntu)
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}

Ce bloc utilise les data sources Terraform pour interroger l'infrastructure existante d'AWS sans la recréer :

  • data "aws_vpc" "default" : récupère le VPC par défaut
  • data "aws_subnets" "default" : récupère tous les subnets du VPC
  • data "aws_ami" "ubuntu" : récupère la dernière Ubuntu 22.04

Le groupe se crée vide de règles, et c'est volontaire : les règles d'entrée sont des ressources distinctes, ajoutées à l'étape suivante, ce qui permet d'en modifier une sans toucher au groupe. L'argument vpc_id le rattache au VPC lu plus haut, car un security group n'existe jamais hors d'un VPC.

Toujours dans main.tf, ajoutez :

resource "aws_security_group" "lab02_sg" {
name = "lab02-sg"
description = "Security group for Lab 02 - SSH and HTTP access"
vpc_id = data.aws_vpc.default.id
tags = {
Name = "lab02-sg"
}
}

Un security group est un firewall virtuel pour vos instances. Vous allez lui ajouter des règles ensuite.

Chaque règle est une ressource à part entière, reliée au groupe par security_group_id. La valeur 0.0.0.0/0 ouvre le port à toute adresse d'Internet : acceptable sur un lab jetable, à resserrer partout ailleurs, comme le rappellent les bonnes pratiques plus bas.

Toujours dans main.tf, ajoutez les règles d'ingress :

# Autoriser SSH (port 22)
resource "aws_vpc_security_group_ingress_rule" "ssh" {
security_group_id = aws_security_group.lab02_sg.id
description = "Allow SSH from anywhere"
from_port = 22
to_port = 22
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
tags = {
Name = "allow-ssh"
}
}
# Autoriser HTTP (port 80)
resource "aws_vpc_security_group_ingress_rule" "http" {
security_group_id = aws_security_group.lab02_sg.id
description = "Allow HTTP from anywhere"
from_port = 80
to_port = 80
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
tags = {
Name = "allow-http"
}
}

Chaque règle dit la même chose : « autoriser le trafic TCP entrant sur ce port, depuis n'importe quelle adresse ».

Étape 6, Créer l'instance dans le subnet avec le security group

Section intitulée « Étape 6, Créer l'instance dans le subnet avec le security group »

Toujours dans main.tf, complétez :

resource "aws_instance" "lab02_vm" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = data.aws_subnets.default.ids[0]
vpc_security_group_ids = [aws_security_group.lab02_sg.id]
tags = {
Name = var.instance_name
}
}

Trois arguments portent tout le câblage de ce bloc :

  • subnet_id place l'instance dans le subnet sélectionné
  • vpc_security_group_ids associe le security group à l'instance
  • la valeur [aws_security_group.lab02_sg.id] est une liste : contrairement à security_groups, réservé au VPC par défaut, vpc_security_group_ids est explicite et recommandé

Ces sorties servent de vérification : elles affichent, sans ouvrir la console, le subnet réellement retenu, l'identifiant du security group et les deux adresses de l'instance. Comparer instance_private_ip et instance_public_ip montre d'un coup d'œil si la machine a bien reçu une adresse publique.

Créez outputs.tf :

output "vpc_id" {
description = "VPC ID"
value = data.aws_vpc.default.id
}
output "subnet_id" {
description = "Subnet ID where instance is running"
value = aws_instance.lab02_vm.subnet_id
}
output "security_group_id" {
description = "Security Group ID"
value = aws_security_group.lab02_sg.id
}
output "instance_id" {
description = "EC2 Instance ID"
value = aws_instance.lab02_vm.id
}
output "instance_public_ip" {
description = "Public IP address of the instance"
value = aws_instance.lab02_vm.public_ip
}
output "instance_private_ip" {
description = "Private IP address of the instance"
value = aws_instance.lab02_vm.private_ip
}
output "ubuntu_ami_id" {
description = "Ubuntu AMI ID"
value = data.aws_ami.ubuntu.id
}

Terraform charge automatiquement terraform.tfvars s'il existe, sans option de ligne de commande. Ce fichier rassemble en un seul endroit ce que vous changerez d'un lab à l'autre, la région en tête.

Créez terraform.tfvars :

aws_region = "us-east-1"
instance_name = "lab02-instance-with-sg"
instance_type = "t3.micro"

Les quatre commandes ci-dessous s'enchaînent toujours dans cet ordre. terraform validate ne contacte pas AWS, il ne vérifie que la syntaxe et la cohérence des références. C'est terraform plan qui interroge réellement l'API pour résoudre les data sources, d'où les valeurs de subnet_id et ubuntu_ami_id déjà connues avant l'apply.

  1. Initialiser Terraform :

    Fenêtre de terminal
    terraform init

    Sortie attendue :

    Initializing the backend...
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching "~> 5.0"...
    - Installing hashicorp/aws v5.100.0...
    Terraform has been successfully initialized!
  2. Valider la configuration :

    Fenêtre de terminal
    terraform validate

    Sortie attendue :

    Success! The configuration is valid.
  3. Afficher le plan :

    Fenêtre de terminal
    terraform plan

    Sortie attendue (résumé) :

    Plan: 4 to add, 0 to change, 0 to destroy.
    Changes to Outputs:
    + instance_id = (known after apply)
    + instance_private_ip = (known after apply)
    + instance_public_ip = (known after apply)
    + security_group_id = (known after apply)
    + subnet_id = "subnet-xxxxxxxxx"
    + ubuntu_ami_id = "ami-xxxxxxxx"
    + vpc_id = "vpc-xxxxxxxx"
  4. Appliquer la configuration :

    Fenêtre de terminal
    terraform apply -auto-approve

    Sortie attendue (fin) :

    Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
    Outputs:
    instance_id = "i-0e43f24bc8817d50d"
    instance_private_ip = "172.31.69.130"
    instance_public_ip = "203.0.113.10"
    security_group_id = "sg-07b0de0b4eca49e47"
    subnet_id = "subnet-ef519de1"
    ubuntu_ami_id = "ami-00de3875b03809ec5"
    vpc_id = "vpc-801432fa"
  5. Vérifier l'accès SSH (optionnel, la clé par défaut n'est pas disponible) :

    Fenêtre de terminal
    aws ec2 describe-instances --instance-ids i-0e43f24bc8817d50d

Maintenant que l'infrastructure est en place, revenons sur les deux décisions structurantes : l'emploi des data sources plutôt que de nouvelles ressources, et la séparation des règles d'ingress hors du bloc aws_security_group. Deux choix qui reviendront dans toutes vos configurations AWS.

La distinction se voit dans le plan : les data sources n'apparaissent jamais dans le décompte « to add », seules les resource y figurent. Ici, quatre ressources sont créées alors que sept objets sont manipulés.

TypeUtilitéExemple
Data sourceLire de l'infrastructure existantedata "aws_vpc", data "aws_ami"
ResourceCréer nouvelle infrastructureresource "aws_security_group", resource "aws_instance"

Les data sources ne créent rien, elles lisent uniquement. Cela vous permet de réutiliser ce qui existe déjà (VPC par défaut, AMI) sans duplication.

Deux approches pour les règles :

# Approche 1 : Règles DANS la ressource (inline), déprécié
resource "aws_security_group" "example" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Approche 2 : Ressources SÉPARÉES (recommended) ✅
resource "aws_security_group" "example" {
name = "example"
}
resource "aws_vpc_security_group_ingress_rule" "ssh" {
security_group_id = aws_security_group.example.id
from_port = 22
to_port = 22
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
}

Pourquoi séparer ?

  • Plus granulaire : vous pouvez ajouter/modifier/supprimer une seule règle sans refactoriser tout le security group
  • Plus lisible : chaque règle est explicite
  • Meilleure gestion en équipe : moins de conflits lors de merges

Un security group est l'un des rares objets AWS dont une erreur de configuration est directement exploitable depuis Internet. Les quatre règles ci-dessous portent sur deux axes : rendre les groupes identifiables pour pouvoir les auditer, et réduire la surface exposée.

Le champ name est celui qu'affiche la console AWS, et il est unique par VPC : un groupe nommé sg devient impossible à distinguer dès le troisième projet.

# ❌ Mauvais
resource "aws_security_group" "main" {
name = "sg"
}
# ✅ Bon
resource "aws_security_group" "web_access" {
name = "web-access-sg"
}

Un port 22 ouvert sur 0.0.0.0/0 est scanné et attaqué en quelques minutes. Remplacez-le par le préfixe de votre VPN ou de votre sortie Internet ; pour un accès nomade, préférez une solution sans port exposé comme AWS Systems Manager Session Manager.

# ❌ Risquée
cidr_ipv4 = "0.0.0.0/0"
# ✅ Produit
cidr_ipv4 = "203.0.113.0/24" # Votre bureau/VPN

3. Utiliser des tags pour tracer qui a créé quoi

Section intitulée « 3. Utiliser des tags pour tracer qui a créé quoi »

Sans tag ManagedBy, rien ne distingue dans la console une ressource pilotée par Terraform d'une ressource créée à la main : la modifier depuis la console provoque une dérive que le prochain apply écrasera sans prévenir.

tags = {
Name = "lab02-sg"
Environment = "lab"
ManagedBy = "terraform"
}

La valeur ip_protocol = "-1" signifie tous les protocoles, et elle rend from_port et to_port sans effet : la règle ouvre alors la totalité du trafic entrant, pas seulement les ports listés.

# ❌ Trop permissif
from_port = 0
to_port = 65535
ip_protocol = "-1" # Tous les protocoles
# ✅ Juste le nécessaire
from_port = 22
to_port = 22
ip_protocol = "tcp"

Les erreurs de ce lab se répartissent en deux familles : celles remontées par Terraform pendant l'apply, qui portent un message AWS explicite, et celles qui n'apparaissent qu'après coup, quand l'instance est créée mais injoignable. Pour la seconde, vérifiez toujours l'association du security group avant de soupçonner les règles.

SymptômeCause probableSolution
Error: Invalid index sur ids[0], « the collection has no elements »aws_subnets n'a trouvé aucun subnet dans le VPC par défaut : la data source rend une liste vide, sans erreurCréer un VPC et un subnet, ou choisir la zone avec data "aws_availability_zones"
Error: creating Security Group: ... InvalidParameterValueCaractères spéciaux dans la descriptionUtiliser uniquement ASCII (pas d'accents, tirets longs)
Instance créée mais pas accessible SSHLa clé .pem n'est pas attachéeAjouter key_name = aws_key_pair.xxx.key_name à la ressource
Port 22 ou 80 ferméSecurity group non attaché, ou règle manquanteVérifier vpc_security_group_ids et les ingress rules

Après le lab, détruisez l'infrastructure pour ne pas être facturé :

Fenêtre de terminal
terraform destroy -auto-approve

Sortie attendue :

Destroy complete! Resources: 4 destroyed.

Nettoyez les fichiers Terraform générés :

Fenêtre de terminal
rm -rf .terraform* terraform.tfstate*

Vous construisez un security group dont toutes les règles sont des ressources dédiées, forme que la documentation recommande désormais à la place des blocs imbriqués. Les règles d'entrée se factorisent par for_each, et l'instance se pose dans un subnet désigné par son tag plutôt que par un identifiant écrit en dur.

  1. Les data sources permettent de lire l'infrastructure existante (VPC, subnet, AMI) sans la recréer
  2. aws_vpc_security_group_ingress_rule est l'approche recommandée pour les règles (séparation des préoccupations)
  3. Les security groups sont des pare-feu virtuels, ils contrôlent qui peut accéder à vos instances
  4. vpc_security_group_ids (liste) est plus robuste que security_groups (approche legacy)
  5. Les outputs vous permettent de voir les IDs importants après l'apply
  6. Testez le plan avant l'apply pour voir exactement ce que Terraform créera

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn