Aller au contenu
Cloud medium

Chapitre 1 — Provisionner les 3 Nets en /22 sur OUTSCALE

20 min de lecture

logo 3ds outscale

Ce chapitre provisionne l'ossature réseau du capstone : 3 Nets /22 (un par sous-région), 9 subnets /24 (3 tiers × 3 Nets), 3 Internet Services, 3 NAT Services et 9 route tables, soit 48 ressources OUTSCALE créées par un seul terraform apply. Vous repartez d'un code factorisé avec for_each et cidrsubnet() — pas de copier-coller par Net. Public visé : intermédiaire à avancé, à l'aise avec Terraform mais pas forcément avec les pièges OUTSCALE-spécifiques (backend OOS, NAT par Net, route table par tier). À la sortie de ce chapitre, l'infrastructure réseau est en place sur le compte cible et prête à recevoir les peerings du Chapitre 3.

  • Structurer un stack Terraform réutilisable avec for_each sur une map(object) de Nets.
  • Dériver les 9 subnets en /24 à partir des 3 CIDR de Net en /22 avec cidrsubnet().
  • Configurer un backend d'état Terraform sur OOS compatible avec le SDK AWS Go v2.
  • Appliquer la discipline 5 tags WAF sur 100 % des ressources via dynamic "tags".
  • Expliquer pourquoi 3 NAT Services (un par Net) plutôt qu'un seul partagé.
  • Distinguer les 3 route tables par Net (public, private, data) et leurs routes par défaut.
  • Avoir lu la vue d'ensemble du capstone — décisions structurantes (RTO/RPO, /22, full-mesh).
  • Avoir réalisé le pré-requis EIP fixe via oapi-cli si vous prévoyez d'enchaîner avec le bastion (référence : oapi-cli).
  • Avoir cloné le dépôt lab-outscale-capstone (à publier sur GitHub outscale-srt20).
  • Profil OSC default configuré dans ~/.osc/config.json (région eu-west-2).
  • Terraform 1.10+ et provider outscale/outscale 1.5.0 pinnés.

Le capstone est découpé en stacks indépendants — un dossier Terraform par couche. Le stack 00-nets/ est la fondation : tout le reste (peering, bastion, app) le consomme via les outputs ou via le state distant.

  • Répertoireterraform/
    • Répertoire00-nets/
      • provider.tf
      • variables.tf
      • locals.tf
      • nets.tf
      • internet.tf
      • routes.tf
      • outputs.tf
    • Répertoire05-peering/
    • Répertoire10-bastion/
    • Répertoire20-app/

Chaque fichier a une responsabilité unique : provider.tf configure le backend et le provider, variables.tf les paramètres exposés à l'opérateur, locals.tf les transformations dérivées, nets.tf les ressources réseau, internet.tf IGW + NAT, routes.tf les 3 route tables × 3 Nets, outputs.tf les maps consommées par les stacks downstream. Cette séparation par fichier facilite les revues — un changement de NAT ne touche pas le fichier des subnets.

La paramétrisation — une map de Nets, pas trois copies

Section intitulée « La paramétrisation — une map de Nets, pas trois copies »

Le piège classique dans un stack multi-AZ est de copier-coller le bloc outscale_net trois fois. C'est invisible au début, douloureux à la première modification commune (changement de tag, ajustement de CIDR, ajout d'un attribut). Le pattern Terraform idiomatique est une map d'objets itérée par for_each.

variable "nets" {
type = map(object({
cidr = string
subregion = string
}))
default = {
"a" = {
cidr = "10.10.0.0/22"
subregion = "eu-west-2a"
}
"b" = {
cidr = "10.11.0.0/22"
subregion = "eu-west-2b"
}
"c" = {
cidr = "10.12.0.0/22"
subregion = "eu-west-2c"
}
}
}

Trois entrées — a, b, c — qui deviendront les clés stables dans tout le code. La clé est volontairement courte : on la retrouve dans les noms de ressources Terraform (outscale_net.main["a"]), dans les tags (Name = "capstone-net-a") et dans les noms de fichier de state. Si vous renommez ces clés plus tard, Terraform recrée toutes les ressources — c'est un choix engageant, à figer le plus tôt possible.

Trois Nets en /22 × 3 tiers (public, private, data) = 9 subnets. Les coder à la main, c'est 9 chaînes IP qui doivent rester cohérentes entre elles. La fonction native cidrsubnet(prefix, newbits, netnum) factorise tout :

locals {
subnets = merge([
for net_key, net in var.nets : {
"${net_key}_public" = {
net_key = net_key
cidr = cidrsubnet(net.cidr, 2, 0) # 10.x.0.0/24
subregion = net.subregion
tier = "public"
}
"${net_key}_private" = {
net_key = net_key
cidr = cidrsubnet(net.cidr, 2, 1) # 10.x.1.0/24
subregion = net.subregion
tier = "private"
}
"${net_key}_data" = {
net_key = net_key
cidr = cidrsubnet(net.cidr, 2, 2) # 10.x.2.0/24
subregion = net.subregion
tier = "data"
}
}
]...)
}

cidrsubnet(net.cidr, 2, N) ajoute 2 bits au préfixe (passe de /22 à /24) et sélectionne le N-ième sous-bloc. Avec N=0,1,2 on prend les 3 premiers /24. Le 4e (N=3) reste en réserve pour un tier futur (monitoring, IoT, GPU…) — c'est tout l'intérêt du /22 plutôt qu'un découpage plus serré.

Le merge([...]...) aplati les 3 maps {public, private, data} en une seule map de 9 entrées indexée par <net>_<tier>. Pratique pour le for_each du outscale_subnet qui suit.

resource "outscale_net" "main" {
for_each = var.nets
ip_range = each.value.cidr
dynamic "tags" {
for_each = merge(local.base_tags, {
Name = "${var.project}-net-${each.key}"
subregion = each.value.subregion
})
content {
key = tags.key
value = tags.value
}
}
}
resource "outscale_subnet" "tier" {
for_each = local.subnets
net_id = outscale_net.main[each.value.net_key].net_id
subregion_name = each.value.subregion
ip_range = each.value.cidr
dynamic "tags" {
for_each = merge(local.base_tags, {
Name = "${var.project}-${each.value.net_key}-${each.value.tier}"
tier = each.value.tier
net = each.value.net_key
})
content {
key = tags.key
value = tags.value
}
}
}

Deux choses à noter. Le dynamic "tags" traduit la map plate {Name=..., project=..., env=...} en blocs tags { key="..." value="..." } exigés par le provider OUTSCALE — sinon il faudrait écrire 5 blocs en dur par ressource. Le outscale_subnet.tier["a_private"].net_id référence le Net parent par sa clé : Terraform en déduit automatiquement la dépendance de création, inutile d'ajouter depends_on.

Internet Service et NAT — un par Net, pas un partagé

Section intitulée « Internet Service et NAT — un par Net, pas un partagé »

C'est le point qui surprend systématiquement les opérateurs venant d'AWS. Chaque Net dispose de sa propre passerelle Internet (Internet Service) et de son propre NAT Service. Pas de gateway centrale, pas de transit Net partagé.

resource "outscale_internet_service" "igw" {
for_each = var.nets
# tags...
}
resource "outscale_internet_service_link" "igw_link" {
for_each = var.nets
internet_service_id = outscale_internet_service.igw[each.key].internet_service_id
net_id = outscale_net.main[each.key].net_id
}
resource "outscale_public_ip" "nat" {
for_each = var.nets
# tags...
}
resource "outscale_nat_service" "nat" {
for_each = var.nets
subnet_id = outscale_subnet.tier["${each.key}_public"].subnet_id
public_ip_id = outscale_public_ip.nat[each.key].public_ip_id
# tags...
}

Pourquoi cette architecture en mesh d'IGW/NAT ? Trois raisons :

  • Pas de SPOF transverse — la perte de la passerelle de Net-B n'impacte ni Net-A ni Net-C.
  • Cohérence du modèle d'isolation — un Net = un périmètre IAM + un périmètre routage. Une gateway partagée casserait l'isolation.
  • Coût visible par Net — chaque NAT Service apparaît dans la facture avec son tag net=<key>, refacturable proprement par projet.

Le prix à payer : 3 NAT Services à payer (au lieu d'un seul mutualisé). Sur le capstone, c'est ~30 €/mois supplémentaires — négligeable au regard des bénéfices d'isolation. Sur un projet à coût serré, on peut décider de partager le NAT en route inter-Net via peering — ce n'est pas le choix de ce capstone (cloisonnement prioritaire).

Trois route tables par Net — public, private, data

Section intitulée « Trois route tables par Net — public, private, data »

Une seule route table par Net suffit techniquement. Le capstone en utilise trois, une par tier. C'est plus de ressources, mais la lisibilité et la sécurité y gagnent largement.

# Public — sortie Internet via IGW
resource "outscale_route_table" "public" {
for_each = var.nets
net_id = outscale_net.main[each.key].net_id
# tags Name = capstone-rt-{key}-public
}
resource "outscale_route" "public_to_igw" {
for_each = var.nets
route_table_id = outscale_route_table.public[each.key].route_table_id
destination_ip_range = "0.0.0.0/0"
gateway_id = outscale_internet_service.igw[each.key].internet_service_id
}
# Private — sortie contrôlée via NAT
resource "outscale_route" "private_to_nat" {
for_each = var.nets
route_table_id = outscale_route_table.private[each.key].route_table_id
destination_ip_range = "0.0.0.0/0"
nat_service_id = outscale_nat_service.nat[each.key].nat_service_id
}
# Data — aucune route 0.0.0.0/0. Volontaire.
resource "outscale_route_table" "data" {
for_each = var.nets
net_id = outscale_net.main[each.key].net_id
# tags Name = capstone-rt-{key}-data
# AUCUNE outscale_route associée
}

Le tier data n'a pas de route Internet, ni entrante ni sortante. La BDD PostgreSQL ne télécharge ni paquet apt ni image Docker ; le runtime applicatif est figé via l'OMI Packer (Chapitre 1). Tous les flux entrants viennent du tier private via Security Groups SG-to-SG, jamais via une route IP. Cette étanchéité de routage est l'une des pratiques fortes du pilier Security du WAF — elle empêche structurellement l'exfiltration accidentelle de données.

Chaque subnet est attaché à sa route table par un outscale_route_table_link :

resource "outscale_route_table_link" "public" {
for_each = var.nets
subnet_id = outscale_subnet.tier["${each.key}_public"].subnet_id
route_table_id = outscale_route_table.public[each.key].route_table_id
}
# Idem pour private et data.

Le backend d'état OOS — paramétrage du SDK AWS

Section intitulée « Le backend d'état OOS — paramétrage du SDK AWS »

Le state Terraform doit vivre ailleurs que sur votre poste dès qu'on dépasse le bac à sable. Pour le capstone, on le pose sur OOS (le service object storage S3-compatible d'OUTSCALE), versionné et chiffré. C'est gratuit en stockage et garde le state dans la même souveraineté que le compute.

terraform {
required_version = "~> 1.10"
required_providers {
outscale = {
source = "outscale/outscale"
version = "1.5.0"
}
}
backend "s3" {
bucket = "capstone-outscale-tfstate"
key = "00-nets/terraform.tfstate"
region = "eu-west-2"
endpoints = {
s3 = "https://oos.eu-west-2.outscale.com"
}
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
use_path_style = true
}
}
provider "outscale" {
api {
region = var.region
}
}

Les flags skip_* neutralisent les vérifications spécifiques AWS qui n'ont pas d'équivalent OOS (pas d'IMDS, pas d'API STS pour valider l'AccountId, etc.). Le use_path_style = true est mandatory sur OOS — l'endpoint accepte https://oos.eu-west-2.outscale.com/<bucket>/... et pas le DNS virtual-hosted <bucket>.oos.eu-west-2.outscale.com. Le bucket est créé une seule fois par un script de bootstrap idempotent (scripts/bootstrap-tfstate-bucket.sh dans le dépôt capstone).

Les outputs — contrats vers les stacks downstream

Section intitulée « Les outputs — contrats vers les stacks downstream »

Le stack 00-nets/ est consommé par 05-peering/, 10-bastion/ et 20-app/. Pour ne pas réécrire les références à chaque fois, on expose 5 maps dans outputs.tf :

output "nets" {
value = {
for k, n in outscale_net.main : k => {
net_id = n.net_id
cidr = n.ip_range
subregion = var.nets[k].subregion
}
}
}
output "subnets" {
value = {
for k, s in outscale_subnet.tier : k => {
subnet_id = s.subnet_id
net_key = local.subnets[k].net_key
tier = local.subnets[k].tier
cidr = s.ip_range
subregion = s.subregion_name
}
}
}
output "route_tables" {
value = {
for k in keys(var.nets) : k => {
public = outscale_route_table.public[k].route_table_id
private = outscale_route_table.private[k].route_table_id
data = outscale_route_table.data[k].route_table_id
}
}
}
# + internet_services, nat_services

Les stacks downstream les lisent via data "terraform_remote_state" :

data "terraform_remote_state" "nets" {
backend = "s3"
config = {
bucket = "capstone-outscale-tfstate"
key = "00-nets/terraform.tfstate"
region = "eu-west-2"
# mêmes endpoints + skip_* que ci-dessus
}
}
# Usage : data.terraform_remote_state.nets.outputs.subnets["a_private"].subnet_id

C'est un contrat d'API stable entre stacks. Si vous renommez une clé d'output (subnetsprivate_subnets), tous les stacks consommateurs cassent. À traiter avec la même rigueur qu'une API publique.

  1. Bootstrap du bucket de state (une seule fois, hors Terraform).

    Fenêtre de terminal
    bash scripts/bootstrap-tfstate-bucket.sh

    Le script crée le bucket capstone-outscale-tfstate en eu-west-2, active le versioning et le chiffrement AES256. Idempotent — relancer ne casse rien.

  2. Charger les variables d'environnement (provider OUTSCALE + checksum fix).

    Fenêtre de terminal
    direnv allow .
    env | grep -E '^(OUTSCALE|AWS)_'

    Doit afficher OUTSCALE_ACCESSKEYID, OUTSCALE_SECRETKEYID, OUTSCALE_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION et les deux variables AWS_*_CHECKSUM_*.

  3. Initialiser le stack et descendre le provider.

    Fenêtre de terminal
    cd terraform/00-nets/
    terraform init

    Sortie attendue : Successfully configured the backend "s3"! + téléchargement du provider outscale/outscale 1.5.0.

  4. Lire le plan AVANT d'appliquer. Vous devez voir 48 ressources à créer :

    Fenêtre de terminal
    terraform plan

    Plan: 48 to add, 0 to change, 0 to destroy. Si vous voyez plus ou moins, arrêtez-vous et vérifiez var.nets (3 entrées attendues).

  5. Appliquer.

    Fenêtre de terminal
    terraform apply

    Compter ~2-3 minutes. La création des NAT Services est l'étape la plus longue (~60 s chacun, parallélisés).

  6. Valider côté OUTSCALE que tout est bien là.

    Fenêtre de terminal
    oapi-cli ReadNets --Filters.Tags '["project=capstone"]' \
    | jq '.Nets | length' # → 3
    oapi-cli ReadSubnets --Filters.Tags '["project=capstone"]' \
    | jq '.Subnets | length' # → 9
    oapi-cli ReadNatServices --Filters.Tags '["project=capstone"]' \
    | jq '.NatServices | length' # → 3

Validations post-apply — au-delà du terraform apply

Section intitulée « Validations post-apply — au-delà du terraform apply »

Le apply réussit ne signifie pas que tout fonctionne fonctionnellement. Trois vérifications complètent le check Terraform :

VérificationCommandeRésultat attendu
Routes par défaut présentesoapi-cli ReadRouteTables --Filters.Tags '["tier=public"]' | jq '.RouteTables[].Routes'Chaque table publique contient 0.0.0.0/0 → igw-XXXX
NAT Services en état availableoapi-cli ReadNatServices | jq '.NatServices[].State'available × 3
Subnets data sans route 0.0.0.0/0oapi-cli ReadRouteTables --Filters.Tags '["tier=data"]' | jq '.RouteTables[].Routes | length'1 (la route locale du Net uniquement)

La 3e validation est la plus importante — elle confirme que le tier data est bien étanche par routage, pas seulement par convention. C'est le test que osc-policy scan live --policy security reproduira automatiquement quand on l'intégrera en CI au Volet 7.

SymptômeCauseSolution
Error: trailing checksum is not supported au initSDK AWS Go v2 envoie un checksum non géré par OOSExporter AWS_REQUEST_CHECKSUM_CALCULATION=when_required et AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Le apply recrée 3 Nets alors qu'ils existent déjàState non sauvegardé sur OOS (cf. ci-dessus) — le précédent apply a planté en write-backNettoyer les ressources orphelines via oapi-cli DeleteNet, fixer le checksum, re-apply
Error: net_peering_id ... blackhole (au Chap. 3)Acceptation de peering oubliéeLe Chapitre 3 explique — outscale_net_peering_acceptation est obligatoire
Subnet créé en eu-west-2a au lieu de bErreur dans var.nets["b"].subregionToujours mettre la subregion avec le suffixe complet (eu-west-2b, pas juste b)
Error: depends_on cycle lors d'un refactordepends_on explicite sur une dépendance déjà impliciteSupprimer le depends_on — la référence par ID suffit
Tag Name absent côté OUTSCALERessource créée sans dynamic "tags"Vérifier que chaque ressource du stack a son bloc tags

Pour rester focalisé, ce chapitre n'aborde pas :

  • Le Net Peering entre les 3 Nets — Chapitre 2.
  • Les Security Groups — créés au Chapitre 3 (bastion) et au Chapitre 4 (app).
  • Le bastion et son EIP fixe — Chapitre 3.
  • La gestion des clés SSH — Chapitre 3.

Si vous tentez de pinger une VM de Net-B depuis Net-A après ce chapitre, ça ne passe pas — c'est normal, les peerings n'existent pas encore.

  • 3 Nets, 9 subnets, 3 IGW, 3 NAT, 9 route tables = 48 ressources, créées en un seul apply factorisé par for_each sur var.nets.
  • La fonction cidrsubnet(cidr, 2, N) dérive 4 subnets /24 à partir d'un /22 — 3 actifs, 1 réserve.
  • Un NAT par Net — la mesh isole les pannes et rend la facture lisible par Net (pilier Cost).
  • Le tier data n'a pas de route Internet, par conception — étanchéité de routage du pilier Security.
  • Le backend OOS réclame use_path_style = true et les deux variables AWS_*_CHECKSUM_* à when_required jusqu'au correctif côté SDK.
  • Les 5 outputs (nets, subnets, internet_services, nat_services, route_tables) sont le contrat d'API stable consommé par tous les stacks downstream.
  • Pas de depends_on quand une référence par ID existe déjà — Terraform infère le graphe correctement.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn