
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Structurer un stack Terraform réutilisable avec
for_eachsur unemap(object)de Nets. - Dériver les 9 subnets en
/24à partir des 3 CIDR de Net en/22aveccidrsubnet(). - 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.
Prérequis
Section intitulée « Prérequis »- 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-clisi 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 GitHuboutscale-srt20). - Profil OSC
defaultconfiguré dans~/.osc/config.json(régioneu-west-2). - Terraform 1.10+ et provider
outscale/outscale1.5.0 pinnés.
Le périmètre du stack 00-nets/
Section intitulée « Le périmètre du stack 00-nets/ »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.
Le découpage des subnets avec cidrsubnet()
Section intitulée « Le découpage des subnets avec cidrsubnet() »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.
Les ressources outscale_net et outscale_subnet
Section intitulée « Les ressources outscale_net et outscale_subnet »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 IGWresource "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 NATresource "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_servicesLes 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_idC'est un contrat d'API stable entre stacks. Si vous renommez une clé d'output (subnets → private_subnets), tous les stacks consommateurs cassent. À traiter avec la même rigueur qu'une API publique.
Étapes — apply pas à pas
Section intitulée « Étapes — apply pas à pas »-
Bootstrap du bucket de state (une seule fois, hors Terraform).
Fenêtre de terminal bash scripts/bootstrap-tfstate-bucket.shLe script crée le bucket
capstone-outscale-tfstateeneu-west-2, active le versioning et le chiffrement AES256. Idempotent — relancer ne casse rien. -
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_REGIONet les deux variablesAWS_*_CHECKSUM_*. -
Initialiser le stack et descendre le provider.
Fenêtre de terminal cd terraform/00-nets/terraform initSortie attendue :
Successfully configured the backend "s3"!+ téléchargement du provideroutscale/outscale 1.5.0. -
Lire le plan AVANT d'appliquer. Vous devez voir 48 ressources à créer :
Fenêtre de terminal terraform planPlan: 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). -
Appliquer.
Fenêtre de terminal terraform applyCompter ~2-3 minutes. La création des NAT Services est l'étape la plus longue (~60 s chacun, parallélisés).
-
Valider côté OUTSCALE que tout est bien là.
Fenêtre de terminal oapi-cli ReadNets --Filters.Tags '["project=capstone"]' \| jq '.Nets | length' # → 3oapi-cli ReadSubnets --Filters.Tags '["project=capstone"]' \| jq '.Subnets | length' # → 9oapi-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érification | Commande | Résultat attendu |
|---|---|---|
| Routes par défaut présentes | oapi-cli ReadRouteTables --Filters.Tags '["tier=public"]' | jq '.RouteTables[].Routes' | Chaque table publique contient 0.0.0.0/0 → igw-XXXX |
NAT Services en état available | oapi-cli ReadNatServices | jq '.NatServices[].State' | available × 3 |
| Subnets data sans route 0.0.0.0/0 | oapi-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.
Pièges courants
Section intitulée « Pièges courants »| Symptôme | Cause | Solution |
|---|---|---|
Error: trailing checksum is not supported au init | SDK AWS Go v2 envoie un checksum non géré par OOS | Exporter 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-back | Nettoyer les ressources orphelines via oapi-cli DeleteNet, fixer le checksum, re-apply |
Error: net_peering_id ... blackhole (au Chap. 3) | Acceptation de peering oubliée | Le Chapitre 3 explique — outscale_net_peering_acceptation est obligatoire |
Subnet créé en eu-west-2a au lieu de b | Erreur dans var.nets["b"].subregion | Toujours mettre la subregion avec le suffixe complet (eu-west-2b, pas juste b) |
Error: depends_on cycle lors d'un refactor | depends_on explicite sur une dépendance déjà implicite | Supprimer le depends_on — la référence par ID suffit |
Tag Name absent côté OUTSCALE | Ressource créée sans dynamic "tags" | Vérifier que chaque ressource du stack a son bloc tags |
Ce qui ne fait PAS partie de ce chapitre
Section intitulée « Ce qui ne fait PAS partie de ce chapitre »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.
À retenir
Section intitulée « À retenir »- 3 Nets, 9 subnets, 3 IGW, 3 NAT, 9 route tables = 48 ressources, créées en un seul
applyfactorisé parfor_eachsurvar.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 = trueet les deux variablesAWS_*_CHECKSUM_*àwhen_requiredjusqu'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_onquand une référence par ID existe déjà — Terraform infère le graphe correctement.