Insset GCP M1 2026M1 Informatique
Tous les projets

Groupe 4 / VM et montée en charge

FlashShop

Une vente flash à préparer

Votre mission

FlashShop prépare une opération commerciale. L’application de démonstration tourne sur une seule machine, et personne ne sait ce qui se passera si elle tombe pendant la vente. On vous demande une plateforme de laboratoire avec plusieurs instances, des machines privées et une capacité qui s’ajuste. Le paiement et le catalogue métier ne font pas partie de votre mission.

« La page doit rester accessible quand une machine tombe. Et je veux comprendre quand la plateforme ajoute de la capacité, combien de temps ça prend et jusqu’où elle peut aller. »

Ce dont le produit a besoin

  • Servir une page légère depuis plusieurs instances, derrière un point d’entrée unique.
  • Garder les VM sans IP publique, tout en leur permettant les sorties dont elles ont besoin.
  • Retirer du trafic une instance défaillante et retrouver une capacité saine.
  • Mesurer comment la capacité réagit à une charge bornée.

Le périmètre du laboratoire

Vous réalisez le VPC, les VM privées, l’Instance Template, un MIG régional, le Load Balancer, les health checks, l’autoscaling et Cloud NAT. Pour Cloud Armor, une règle simple si vous en avez le temps, sinon une conception argumentée.

Le formateur vous donnera les volumes métier quand vous les lui demanderez. Ils servent à raisonner, pas à être reproduits : les essais de charge du laboratoire ont leurs propres limites, fixées à l’étape 5.

Le cœur du module, c’est l’infrastructure

L’application sert seulement à faire passer des requêtes ou des messages. Votre travail porte sur Terraform, le réseau, IAM, le pipeline, le monitoring et la reprise. Personne ne vous demande une application métier complète, et elle ne sera pas évaluée. Le kit de démarrage est là pour vous éviter de l’écrire.

Ce qui compte en premier

Le parcours suit six étapes, et le temps passe vite. Le socle, c’est ce que vous devez pouvoir montrer quoi qu’il arrive. La version complète vient ensuite, si le temps le permet. Un socle solide et bien expliqué vaut mieux qu’une version complète vérifiée à moitié. À chaque étape, notez en quelques lignes ce que vous avez décidé et ce que vous avez mesuré.

ÉtapeSocleVersion complète
01 CadrerUne fiche d’hypothèses et trois critères de réussite validés par le formateur.Les volumes obtenus, une limite de coût et ce que vous écartez, écrits noir sur blanc.
02 ArchitectureLe schéma cible expliqué flux par flux, avec votre découpage Terraform.Trois choix d’infrastructure argumentés, avec les alternatives écartées.
03 ConstruireLe chemin nominal déployé par Terraform, avec un état distant et des identités dédiées : des réponses de deux instances privées derrière le Load Balancer.Des modules documentés, un plan sans dérive et un test négatif, c’est-à-dire un refus ou une erreur attendus.
04 AutomatiserUne CI qui s’authentifie par fédération d’identité et lance fmt, validate et plan à chaque changement.L’apply du plan relu après approbation, un test du parcours nominal et une destruction approuvée.
05 ÉprouverUn dashboard, une alerte, un essai de charge borné et une analyse argumentée de la nouvelle contrainte.Deux alertes dont la notification a bien été reçue, une expérience menée jusqu’au retour au service, et un essai borné sur la nouvelle contrainte.
06 Présenter20 minutes sur ce qui fonctionne vraiment, un ordre de grandeur des coûts et le nettoyage des ressources.Un calcul de coûts détaillé et les écarts avec une cible de production.

Avant de commencer

Avant de créer la moindre ressource, faites le point avec le formateur :

  • Le projet GCP du laboratoire. La facturation doit être active, les API et les quotas disponibles, et vous devez pouvoir créer les ressources et déléguer les permissions nécessaires.
  • La région, le préfixe de votre groupe, les labels à poser et le plafond de dépense. Une alerte de budget prévient, elle ne coupe rien.
  • Vos outils : Terraform, Git et gcloud. Si votre sujet utilise des conteneurs, il vous faut aussi de quoi construire une image et un registre pour la stocker.
  • Un dépôt Git pour le groupe et une plateforme de CI. En local, vous travaillez avec les identifiants prévus par le cours. En CI, vous préparerez une identité fédérée aux droits limités.
  • Un endroit où ranger vos preuves : schémas, captures, résultats horodatés et décisions, sans aucun secret. L’un exécute, l’autre relit, le troisième mesure, et vous changez de rôle régulièrement.

Votre jeu de démonstration

Une page ou une route HTTP qui renvoie l’identifiant de l’instance et la version de l’application. Le démarrage doit être automatique et reproductible. Aucune boutique ni transaction à coder. Le script de démarrage du kit ci-dessous fait exactement cela.

Avant de continuer

Vous savez dans quel projet vous travaillez, ce que vous avez le droit de créer et comment tout arrêter puis supprimer. S’il vous manque un accès, demandez-le plutôt que de le contourner avec des permissions trop larges.

Kit de démarrage

Ce kit vous évite d’écrire l’application. Il contient le code à déployer, avec une interface qui montre ce que fait votre infrastructure, et un docker compose qui imite les services GCP pour tout faire tourner sur votre machine avant le premier déploiement. Il ne contient pas de Terraform : l’écrire reste le cœur du TP.

Vous pouvez le modifier, mais il ne sera pas évalué. Lisez-le avant de l’utiliser, car vous devrez pouvoir l’expliquer comme n’importe quel code que vous n’avez pas écrit vous-même.

Un script de démarrage pour les VM du MIG. Il installe, sans rien télécharger, une page qui montre quelle instance répond derrière le Load Balancer, et un petit générateur de charge borné à 20 requêtes par seconde. La version affichée vient de la métadonnée app-version du template. En local, trois conteneurs construits depuis ce même script jouent les VM de trois zones, derrière un nginx qui joue le Load Balancer.

Télécharger le kit, archive zip

RôleEn localSur GCP
Point d’entréenginx, local/lb.confApplication Load Balancer et backend service
Instancestrois conteneurs, un par zoneVM privées du MIG régional
Retrait d’une instance arrêtéele DNS de Docker ne la renvoie plushealth check du backend service
Remplacement et ajout de capaciténon imitésautohealing et autoscaler du MIG

Pour l’essayer : décompressez l’archive, puis lancez docker compose up --build dans le dossier flashshop et ouvrez http://localhost:8080. Le README de l’archive explique comment passer sur GCP.

vm/startup.sh, le script de démarrage
#!/bin/bash
# Script de démarrage FlashShop pour une image Debian 12.
# À passer au template avec l'argument metadata_startup_script et la fonction file() de Terraform.
# Il installe une page HTTP sur le port 8080 avec le Python déjà présent sur l'image :
# aucun téléchargement n'est nécessaire. Il peut être rejoué sans risque à chaque démarrage.
#
# Routes : /               page qui montre quelle instance répond, avec un petit générateur de charge
#          /api/instance   identité de l'instance qui a répondu (JSON)
#          /api/travail    même chose après un calcul de ms millisecondes, pour charger le CPU
#          /api/deploiement  ce que l'instance constate de son propre déploiement
#          /healthz        pour les health checks du Load Balancer et de l'autohealing
set -euo pipefail

cat > /opt/flashshop.py <<'PY'
import ipaddress
import itertools
import json
import os
import socket
import threading
import time
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


def metadata(path, default):
    request = urllib.request.Request(
        "http://metadata.google.internal/computeMetadata/v1/" + path,
        headers={"Metadata-Flavor": "Google"},
    )
    try:
        with urllib.request.urlopen(request, timeout=2) as response:
            return response.read().decode()
    except Exception:
        return default


# Sur Compute Engine, tout vient du serveur de métadonnées. En local, des variables d'environnement
# prennent le relais. La version vient de la métadonnée app-version du template.
SUR_GCE = not os.environ.get("INSTANCE_NAME") and metadata("instance/id", None) is not None
INSTANCE = os.environ.get("INSTANCE_NAME") or metadata("instance/name", socket.gethostname())
ZONE = (os.environ.get("ZONE") or metadata("instance/zone", "inconnue")).rsplit("/", 1)[-1]
VERSION = os.environ.get("APP_VERSION") or metadata("instance/attributes/app-version", "dev")
EXTERNE = metadata("instance/network-interfaces/0/access-configs/0/external-ip", None) if SUR_GCE else None
CREE_PAR = metadata("instance/attributes/created-by", "") if SUR_GCE else ""
DEMARRAGE = time.time()
REQUETES = itertools.count(1)

# Démo publique : charge plafonnée, calcul court et nombre de requêtes limité par seconde.
DEMO = os.environ.get("DEMO_PUBLIQUE") == "true"
CHARGE_MAX = int(os.environ.get("CHARGE_MAX", "5" if DEMO else "20"))
TRAVAIL_MAX_MS = 20 if DEMO else 500

# Plages documentées des sondes de health check des Load Balancers Google.
PLAGES_SONDES = [ipaddress.ip_network("35.191.0.0/16"), ipaddress.ip_network("130.211.0.0/22")]
etat = {"sondes": 0, "derniere_sonde": 0.0, "sortie": None}
verrou = threading.Lock()


def tester_sortie():
    # Sans IP externe, une sortie qui réussit passe forcément par Cloud NAT.
    while True:
        try:
            socket.create_connection(("deb.debian.org", 443), timeout=3).close()
            etat["sortie"] = True
        except OSError:
            etat["sortie"] = False
        time.sleep(300)


class Limiteur:
    def __init__(self, par_seconde):
        self.capacite = self.jetons = par_seconde
        self.maj = time.monotonic()
        self.verrou = threading.Lock()

    def autoriser(self):
        with self.verrou:
            maintenant = time.monotonic()
            self.jetons = min(self.capacite, self.jetons + (maintenant - self.maj) * self.capacite)
            self.maj = maintenant
            if self.jetons < 1:
                return False
            self.jetons -= 1
            return True


LIMITEUR = Limiteur(30) if DEMO else None


def bloc(id, nom, etat_bloc, detail="", preuve=""):
    return {"id": id, "nom": nom, "etat": etat_bloc, "detail": detail, "preuve": preuve}


def carte():
    if not SUR_GCE:
        blocs = [
            bloc("vm", "VM du groupe", "simule", f"conteneur {INSTANCE}, zone {ZONE}, version {VERSION}"),
            bloc("ip", "Adresse des VM", "simule", "les VM du compose ne publient aucun port"),
            bloc("mig", "Groupe d’instances", "simule", "trois conteneurs du compose, un par zone"),
            bloc("sondes", "Health checks", "simule", "nginx ne sonde pas les VM en local"),
            bloc("nat", "Sortie Internet", "simule", "sortie directe du conteneur"),
        ]
    else:
        blocs = [bloc("vm", "VM du groupe", "ok", f"instance {INSTANCE}, zone {ZONE}, version {VERSION}", "serveur de métadonnées")]
        blocs.append(bloc("ip", "Adresse des VM", "alerte", f"cette VM a une IP externe : {EXTERNE}", "access-config présent")
                     if EXTERNE else bloc("ip", "Adresse des VM", "ok", "aucune IP externe sur cette VM", "aucun access-config dans les métadonnées"))
        if "/instanceGroupManagers/" in CREE_PAR:
            genre = "MIG régional" if "/regions/" in CREE_PAR else "MIG zonal"
            blocs.append(bloc("mig", "Groupe d’instances", "ok", f"{genre} {CREE_PAR.rsplit('/', 1)[-1]}", CREE_PAR))
        else:
            blocs.append(bloc("mig", "Groupe d’instances", "inconnu", "cette VM n’a pas été créée par un MIG"))
        with verrou:
            sondes, derniere = etat["sondes"], etat["derniere_sonde"]
        blocs.append(bloc("sondes", "Health checks", "ok", f"{sondes} sonde(s) reçue(s) depuis les plages Google, la dernière il y a {int(time.time() - derniere)} s", "35.191.0.0/16 et 130.211.0.0/22")
                     if sondes else bloc("sondes", "Health checks", "inconnu", "aucune sonde reçue : health check créé, pare-feu ouvert aux plages des sondes ?"))
        if etat["sortie"] is None:
            blocs.append(bloc("nat", "Sortie Internet", "inconnu", "test de sortie en cours"))
        elif etat["sortie"] and not EXTERNE:
            blocs.append(bloc("nat", "Sortie par Cloud NAT", "ok", "sortie Internet possible sans IP externe", "connexion à deb.debian.org:443 réussie"))
        elif etat["sortie"]:
            blocs.append(bloc("nat", "Sortie Internet", "ok", "sortie par l’IP externe de la VM, pas par NAT", "connexion à deb.debian.org:443 réussie"))
        else:
            blocs.append(bloc("nat", "Sortie Internet", "inconnu", "aucune sortie Internet : Cloud Router et NAT en place ?", "connexion à deb.debian.org:443 impossible"))
    blocs += [
        bloc("armor", "Cloud Armor", "manuel", "règle et logs de décision : à montrer dans la console"),
        bloc("autoscaler", "Autoscaler", "manuel", "décisions de capacité : à montrer dans le MIG et Monitoring"),
        bloc("obs", "Monitoring et alertes", "manuel", "dashboard et alertes : à montrer dans la console"),
    ]
    return {"plateforme": "Compute Engine" if SUR_GCE else "local", "blocs": blocs}


PAGE = r"""<!DOCTYPE html>
<html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>FlashShop, démonstrateur</title>
<style>
:root{--ink:#0f172a;--muted:#5b6575;--line:#e2e8f0;--soft:#f6f8fa;--night:#0b1220;--accent:#e0473a;--ok:#0f8a4f;--warn:#b45309;--err:#c5221f;--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}
*{box-sizing:border-box}body{margin:0;background:var(--soft);color:#1e293b;font:15px/1.6 ui-sans-serif,-apple-system,"Segoe UI",Roboto,Arial,sans-serif}
header{background:var(--night);color:#fff}header div{max-width:1100px;margin:auto;padding:12px 24px;display:flex;gap:12px;align-items:center}
.mark{width:10px;height:10px;border-radius:2px;background:var(--accent)}.brand{font:600 15px/1 var(--mono)}#env{margin-left:auto;font:12px/1.4 var(--mono);color:#94a3b8}
main{max-width:1100px;margin:auto;padding:24px;display:grid;gap:16px}h1{margin:0;font-size:26px;color:var(--ink)}.lead{color:var(--muted);margin:4px 0 0}
.panel{background:#fff;border:1px solid var(--line);border-radius:8px;padding:18px}.panel h2{margin:0 0 12px;font:600 12px/1.4 var(--mono);text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}.stat{border:1px solid var(--line);border-radius:6px;padding:10px 12px;background:#fff}
.stat>span{display:block;font:11px/1.4 var(--mono);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.stat strong{font:600 20px/1.35 var(--mono);color:var(--ink)}
button{font:500 13px/1.4 var(--mono);padding:7px 12px;border-radius:5px;border:1px solid #c4ceda;background:#fff;cursor:pointer}button.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.muted{color:var(--muted);font-size:13px}
.barre{display:grid;grid-template-columns:minmax(150px,240px) 1fr 70px;gap:10px;align-items:center;margin:8px 0;font:13px/1.4 var(--mono)}
.barre .fond{height:14px;background:#eef1f4;border-radius:3px;overflow:hidden}.barre .fond span{display:block;height:100%}.barre.muette{opacity:.4}
#cases{display:flex;flex-wrap:wrap;gap:3px}#cases i{width:12px;height:12px;border-radius:2px;display:block}
.badge{display:inline-block;font:600 11px/1.6 var(--mono);padding:1px 7px;border-radius:4px;background:var(--soft);border:1px solid var(--line);color:var(--muted)}
.badge.ok{color:var(--ok);border-color:#b7e1c7;background:#effaf3}.badge.warn{color:var(--warn);border-color:#f3d7a7;background:#fff8ec}.badge.err{color:var(--err);border-color:#f5c2bf;background:#fef3f2}.badge.sim{color:#4f46e5;border-color:#c7d2fe;background:#eef2ff}
.carte{display:grid;gap:12px}.resume{font:13px/1.4 var(--mono);color:var(--muted)}
.rangee{display:grid;grid-template-columns:110px repeat(auto-fit,minmax(170px,1fr));gap:10px}.rangee>span{font:600 11px/1.4 var(--mono);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);padding-top:12px}
.bloc{border:1px solid var(--line);border-left:4px solid #cbd5e1;border-radius:6px;padding:10px 12px;background:#fff;min-width:0}
.bloc strong{display:block;color:var(--ink);margin:6px 0 2px;font-size:14px}.bloc p{margin:0;font-size:13px;line-height:1.45;color:var(--muted)}.bloc code{display:block;margin-top:6px;font:11px/1.4 var(--mono);color:var(--muted);overflow-wrap:anywhere}
.etat-ok{border-left-color:var(--ok);background:#f5fbf7}.etat-alerte{border-left-color:var(--warn);background:#fffaf0}.etat-echec{border-left-color:var(--err);background:#fff6f5}
.etat-inconnu{border-style:dashed;border-left-style:solid}.etat-manuel{border-style:dotted;border-left-style:solid;background:var(--soft)}.etat-simule{border-left-color:#6366f1;background:#f7f7ff}
@media(max-width:760px){.rangee{grid-template-columns:1fr}.rangee>span{padding-top:4px}}
</style></head><body>
<header><div><span class="mark"></span><span class="brand">FlashShop</span><span id="env">...</span></div></header>
<main>
<div><h1>Qui répond derrière le Load Balancer ?</h1><p class="lead">Chaque requête passe par le Load Balancer, qui choisit une instance du MIG. Arrêtez le service sur une VM et regardez sa barre s’arrêter.</p></div>
<section class="panel"><h2>Carte du déploiement</h2><div class="carte" id="carte"><p class="muted">Analyse en cours...</p></div></section>
<div class="stats"><div class="stat"><span>Requêtes</span><strong id="n">0</strong></div><div class="stat"><span>Erreurs</span><strong id="e">0</strong></div><div class="stat"><span>Latence p50</span><strong id="p50">...</strong></div><div class="stat"><span>Latence p95</span><strong id="p95">...</strong></div></div>
<section class="panel"><h2>Charge</h2>
<div class="row"><button data-rate="1" class="on">Observer, 1 req/s</button><button data-rate="5">5 req/s</button><button data-rate="10">10 req/s</button><button data-rate="20">20 req/s</button><button data-rate="0">Arrêter</button>
<label class="muted"><input type="checkbox" id="cpu"> avec du calcul côté serveur</label></div>
<p class="muted" id="minuteur">Les paliers de charge s’arrêtent seuls après 60 secondes. Ne dépassez pas la capacité convenue avec le formateur.</p></section>
<section class="panel"><h2>Répartition entre les instances</h2><div id="barres"></div></section>
<section class="panel"><h2>Dernières réponses</h2><div id="cases"></div><p class="muted">Une case par réponse, de la couleur de l’instance. En rouge : une erreur ou un délai dépassé.</p></section>
</main>
<script>
const $=s=>document.querySelector(s);const vues=new Map();const latences=[];const cases=[];let n=0,erreurs=0,minuterie=null,fin=0,via=null,plateforme=null;
const couleur=id=>'hsl('+([...id].reduce((h,c)=>(h*31+c.charCodeAt(0))%997,7)*137.5)%360+' 70% 50%)';
async function requete(){n++;const t0=performance.now();const url=$('#cpu').checked?'/api/travail?ms=50':'/api/instance';
try{const r=await fetch(url,{cache:'no-store',signal:AbortSignal.timeout(3000)});if(!r.ok)throw new Error(r.status);via=r.headers.get('via')||via;const d=await r.json();
const ms=performance.now()-t0;latences.push(ms);if(latences.length>200)latences.shift();
const v=vues.get(d.instance)||{zone:d.zone,version:d.version,n:0};v.n++;v.vu=Date.now();v.version=d.version;vues.set(d.instance,v);cases.push(couleur(d.instance));}
catch(e){erreurs++;cases.push('var(--err)');}if(cases.length>150)cases.shift();}
function centile(p){if(!latences.length)return'...';const t=[...latences].sort((a,b)=>a-b);return Math.round(t[Math.min(t.length-1,Math.floor(p*t.length))])+' ms';}
function dessiner(){$('#n').textContent=n;$('#e').textContent=erreurs;$('#p50').textContent=centile(.5);$('#p95').textContent=centile(.95);
const total=[...vues.values()].reduce((s,v)=>s+v.n,0)||1;$('#barres').replaceChildren(...[...vues.entries()].sort().map(([id,v])=>{const d=document.createElement('div');
d.className='barre'+(Date.now()-v.vu>5000?' muette':'');d.innerHTML='<span></span><span class="fond"><span></span></span><span></span>';d.children[0].textContent=id+' / '+v.zone+' / '+v.version;
d.children[1].firstChild.style.width=(v.n/total*100)+'%';d.children[1].firstChild.style.background=couleur(id);d.children[2].textContent=v.n;return d;}));
$('#cases').replaceChildren(...cases.map(c=>{const i=document.createElement('i');i.style.background=c;return i;}));
if(fin){const reste=Math.max(0,Math.round((fin-Date.now())/1000));$('#minuteur').textContent='Palier en cours, encore '+reste+' s.';if(!reste)regler(1);}}
function regler(rate){clearInterval(minuterie);minuterie=null;fin=0;document.querySelectorAll('button[data-rate]').forEach(b=>b.classList.toggle('on',Number(b.dataset.rate)===rate));
if(rate>0)minuterie=setInterval(requete,1000/rate);if(rate>1)fin=Date.now()+60000;else $('#minuteur').textContent='Les paliers de charge s’arrêtent seuls après 60 secondes.';}
document.querySelectorAll('button[data-rate]').forEach(b=>b.addEventListener('click',()=>regler(Number(b.dataset.rate))));
const etats={ok:['prouvé','ok'],alerte:['à revoir','warn'],echec:['en échec','err'],inconnu:['pas encore détecté',''],manuel:['à prouver vous-même',''],simule:['simulé en local','sim']};
const bloc=(id,nom,etat,detail='',preuve='')=>({id,nom,etat,detail,preuve});
async function carte(){let s;try{s=await(await fetch('/api/deploiement',{cache:'no-store'})).json();}catch{return;}
const local=s.plateforme==='local';const blocs=[...s.blocs];const zones=new Set([...vues.values()].map(v=>v.zone));
blocs.push(local?bloc('lb','Load Balancer','simule','nginx, local/lb.conf'):via&&/google/i.test(via)?bloc('lb','Load Balancer','ok','les réponses passent par un Load Balancer Google','via: '+via):bloc('lb','Load Balancer','inconnu','aucun en-tête Via de Google : la page passe-t-elle par le Load Balancer ?'));
const z=[...zones].sort().join(', ');blocs.push(zones.size>1?bloc('zones','Répartition entre zones',local?'simule':'ok','réponses venues de '+zones.size+' zones et '+vues.size+' instances',z):bloc('zones','Répartition entre zones',local?'simule':'inconnu',zones.size?'une seule zone vue pour l’instant : '+z:'en attente de réponses'));
const rangees=[['Entrée',['lb','armor']],['Flotte',['mig','zones','vm','ip']],['Santé',['sondes']],['Sorties',['nat']],['Exploitation',['autoscaler','obs']]];
const parId=new Map(blocs.map(b=>[b.id,b]));const lignes=rangees.map(([titre,ids])=>{const r=document.createElement('div');r.className='rangee';const t=document.createElement('span');t.textContent=titre;r.append(t);
for(const id of ids){const b=parId.get(id);if(!b)continue;const[texte,classe]=etats[b.etat]||etats.inconnu;const c=document.createElement('div');c.className='bloc etat-'+b.etat;
c.innerHTML='<span class="badge"></span><strong></strong><p></p><code></code>';c.children[0].className='badge '+classe;c.children[0].textContent=texte;c.children[1].textContent=b.nom;c.children[2].textContent=b.detail;
if(b.preuve)c.children[3].textContent=b.preuve;else c.children[3].remove();r.append(c);}return r;});
const det=blocs.filter(b=>b.etat!=='manuel');const ok=det.filter(b=>b.etat==='ok').length;const resume=document.createElement('p');resume.className='resume';
resume.textContent=det.every(b=>b.etat==='simule')?'En local, tous les blocs sont simulés. Une fois déployée sur GCP, la carte s’allume bloc par bloc.':ok+' bloc(s) prouvé(s) sur '+det.length+' détectables.';
$('#carte').replaceChildren(resume,...lignes);}
fetch('/api/instance',{cache:'no-store'}).then(r=>r.json()).then(d=>{$('#env').textContent='page servie par '+d.instance+' / '+d.zone+' / '+d.version;
document.querySelectorAll('button[data-rate]').forEach(b=>{if(Number(b.dataset.rate)>d.charge_max)b.remove();});});
regler(1);setInterval(dessiner,500);carte();setInterval(carte,10000);
</script></body></html>
""".encode()


class Handler(BaseHTTPRequestHandler):
    def repondre(self, status, body, kind):
        self.send_response(status)
        self.send_header("Content-Type", kind)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def json(self, contenu, status=200):
        self.repondre(status, json.dumps(contenu).encode() + b"\n", "application/json; charset=utf-8")

    def identite(self, **extra):
        return {"instance": INSTANCE, "zone": ZONE, "version": VERSION, "depuis_s": int(time.time() - DEMARRAGE),
                "requetes": next(REQUETES), "charge_max": CHARGE_MAX, **extra}

    def do_GET(self):
        url = urllib.parse.urlsplit(self.path)
        if url.path == "/healthz":
            try:
                source = ipaddress.ip_address(self.client_address[0])
                if any(source in plage for plage in PLAGES_SONDES):
                    with verrou:
                        etat["sondes"] += 1
                        etat["derniere_sonde"] = time.time()
            except ValueError:
                pass
            return self.repondre(200, b"ok\n", "text/plain; charset=utf-8")
        if url.path == "/":
            return self.repondre(200, PAGE, "text/html; charset=utf-8")
        if url.path.startswith("/api/") and LIMITEUR and not LIMITEUR.autoriser():
            return self.json({"erreur": "trop de requêtes pour la démo publique"}, 429)
        if url.path == "/api/instance":
            return self.json(self.identite())
        if url.path == "/api/travail":
            try:
                ms = min(TRAVAIL_MAX_MS, max(0, int(urllib.parse.parse_qs(url.query).get("ms", ["50"])[0])))
            except ValueError:
                ms = 20
            fin = time.perf_counter() + ms / 1000
            while time.perf_counter() < fin:
                pass
            return self.json(self.identite(travail_ms=ms))
        if url.path == "/api/deploiement":
            return self.json(carte())
        self.send_error(404)

    def log_message(self, *args):
        pass  # les journaux du Load Balancer suffisent


threading.Thread(target=tester_sortie, daemon=True).start()
serveur = ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8080"))), Handler)
print(f"FlashShop prêt sur le port {serveur.server_address[1]}, instance {INSTANCE}", flush=True)
serveur.serve_forever()
PY

cat > /etc/systemd/system/flashshop.service <<'UNIT'
[Unit]
Description=FlashShop, page de démonstration
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/flashshop.py
Restart=on-failure
DynamicUser=yes

[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
systemctl enable flashshop.service
systemctl restart flashshop.service
docker-compose.yml, le lancement local
# FlashShop en local : docker compose up --build, puis http://localhost:8080
name: flashshop-local

# Trois conteneurs jouent les VM du MIG régional, une par zone, tous construits depuis la même image,
# comme les instances créées à partir d'un même template.
x-vm: &vm
  image: flashshop-vm:local
  build:
    context: .
    dockerfile: local/Dockerfile.vm

services:
  vm-europe-west9-a:
    <<: *vm
    environment: { INSTANCE_NAME: flashshop-mig-a1, ZONE: europe-west9-a, APP_VERSION: v1 }
  vm-europe-west9-b:
    <<: *vm
    environment: { INSTANCE_NAME: flashshop-mig-b1, ZONE: europe-west9-b, APP_VERSION: v1 }
  vm-europe-west9-c:
    <<: *vm
    environment: { INSTANCE_NAME: flashshop-mig-c1, ZONE: europe-west9-c, APP_VERSION: v1 }

  # Joue le rôle de l'Application Load Balancer. Les VM, elles, ne publient aucun port.
  lb:
    image: nginx:stable-alpine
    ports:
      - "8080:80"
    volumes:
      - ./local/lb.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - vm-europe-west9-a
      - vm-europe-west9-b
      - vm-europe-west9-c
local/lb.conf, le Load Balancer imité
# Émulation locale de l'Application Load Balancer devant le MIG.
# Ce fichier ne se déploie jamais : sur GCP, ce rôle revient au backend service,
# à son health check et à l'autohealing du MIG.

# Le DNS de Docker est relu en continu (valid=5s) : une VM arrêtée sort de la liste,
# une VM relancée y revient, comme un backend qui redevient sain.
resolver 127.0.0.11 valid=5s ipv6=off;

upstream mig {
    zone mig 64k;
    server vm-europe-west9-a:8080 max_fails=1 fail_timeout=10s resolve;
    server vm-europe-west9-b:8080 max_fails=1 fail_timeout=10s resolve;
    server vm-europe-west9-c:8080 max_fails=1 fail_timeout=10s resolve;
}

server {
    listen 80;
    location / {
        proxy_pass http://mig;
        # Pas de nouvel essai sur une autre instance : les erreurs restent visibles côté client.
        proxy_next_upstream off;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
    }
}
README.md, le passage sur GCP
# FlashShop, kit de démarrage

Un script de démarrage pour les VM du MIG. Il installe une page qui montre quelle instance répond derrière le Load Balancer, avec un petit générateur de charge borné. Le même code tourne en local, sur trois conteneurs qui jouent les VM de trois zones. Le kit est un démonstrateur : il sert à vérifier votre infrastructure, il n’est pas évalué.

## Ce qu’il contient

```text
vm/startup.sh         Le script à passer au template d’instance (metadata_startup_script)
local/                Ce qui remplace GCP en local, jamais déployé
  Dockerfile.vm       Une « VM » locale, construite à partir de startup.sh
  lb.conf             nginx dans le rôle de l’Application Load Balancer
docker-compose.yml    Trois VM, une par zone, derrière le Load Balancer local
```

## Lancer en local

```sh
docker compose up --build
```

Ouvrez http://localhost:8080. La page interroge le Load Balancer une fois par seconde et montre la répartition des réponses entre les instances. Les boutons de charge envoient 5, 10 ou 20 requêtes par seconde pendant 60 secondes, avec ou sans calcul côté serveur.

Pour simuler la perte d’une VM, puis son retour :

```sh
docker compose stop vm-europe-west9-b
docker compose start vm-europe-west9-b
```

Sa barre s’arrête de grandir, quelques erreurs peuvent apparaître, puis le trafic se répartit sur les deux autres.

| Rôle | En local | Sur GCP |
| --- | --- | --- |
| Point d’entrée | nginx, `local/lb.conf` | Application Load Balancer et backend service |
| Instances | trois conteneurs construits depuis `startup.sh` | VM privées du MIG régional, créées depuis le template |
| Retrait d’une instance | le DNS de Docker ne la renvoie plus | health check du backend service |
| Remplacement d’une instance | à faire à la main avec `docker compose start` | autohealing du MIG |
| Ajout de capacité | non imité | autoscaler du MIG |
| Sorties vers Internet | directes | Cloud NAT |

Le Load Balancer local montre le principe de la répartition et du retrait d’une instance. Il ne reproduit ni les délais des health checks de GCP, ni l’autohealing, ni l’autoscaling.

## Passer sur GCP

Le script `vm/startup.sh` se donne tel quel au template d’instance, par exemple avec `metadata_startup_script = file("startup.sh")` dans votre module. Il n’a besoin d’aucun accès à Internet : le Python présent sur l’image Debian 12 suffit.

| Élément | Valeur |
| --- | --- |
| Port de l’application | `8080`, à déclarer comme named port du groupe d’instances |
| Chemin des health checks | `/healthz` |
| Version affichée | métadonnée `app-version` du template, pour prouver quelle version est servie |
| Service systemd | `flashshop` : `sudo systemctl stop flashshop` sur une VM pour l’expérience de panne |
| Charge CPU | `/api/travail?ms=50`, plafonné à 500 ms par requête |

Les noms d’instance et de zone viennent du serveur de métadonnées de Compute Engine. En local, les variables `INSTANCE_NAME`, `ZONE` et `APP_VERSION` les remplacent.

Le générateur de charge de la page est volontairement limité à 20 requêtes par seconde et à 60 secondes par palier, pour rester dans les bornes du laboratoire. Si aucun scale-out ne se produit, expliquez pourquoi plutôt que d’augmenter la charge.

## La carte du déploiement

En haut de l’interface, l’architecture cible est dessinée bloc par bloc. Chaque bloc s’allume selon ce que l’application constate elle-même, avec la preuve affichée dessous :

| Couleur | Sens |
| --- | --- |
| vert, « prouvé » | l’application l’a vérifié elle-même |
| orange, « à revoir » | ça fonctionne, mais c’est un anti-pattern connu |
| rouge, « en échec » | l’application a essayé et ça ne marche pas |
| pointillés, « pas encore détecté » | rien de visible pour l’instant |
| gris, « à prouver vous-même » | invisible depuis l’application : montrez-le dans la console |
| violet, « simulé en local » | l’équivalent local, en attendant le déploiement |

La carte constate, elle ne note pas : un bloc vert ne dit pas que votre choix est le bon, seulement qu’il est en place.

Pour FlashShop, chaque VM constate elle-même :

| Bloc | Comment |
| --- | --- |
| VM et adresse | serveur de métadonnées : nom, zone, version, présence d’une IP externe |
| Groupe d’instances | métadonnée `created-by` : MIG régional ou zonal |
| Health checks | sondes reçues sur `/healthz` depuis les plages `35.191.0.0/16` et `130.211.0.0/22`, ce qui prouve aussi la règle de pare-feu |
| Sortie par Cloud NAT | une connexion sortante qui réussit sans IP externe |

Le navigateur ajoute le passage par un Load Balancer Google (en-tête `Via`) et le nombre de zones qui ont répondu. Aucun droit IAM n’est nécessaire : tout passe par le serveur de métadonnées.

## Démo publique

| Variable | Rôle |
| --- | --- |
| `DEMO_PUBLIQUE` | `true` : calcul plafonné à 20 ms, 30 requêtes par seconde au plus par instance. |
| `CHARGE_MAX` | Palier de charge le plus haut proposé par la page : 20 par défaut, 5 en démo publique. |

01 Cadrer

Comprendre la demande

Laissez Terraform de côté pour l’instant. Partez du message du responsable et traduisez-le en un parcours utilisateur et en critères de réussite que l’on peut observer.

À faire

  • Choisissez dans la liste ci-dessous les trois questions qui vous semblent prioritaires et posez-les au formateur. Les autres pourront venir ensuite.
  • Séparez ce qui est confirmé, ce que vous supposez pour l’instant et ce qui reste à décider.
  • Fixez un minimum démontrable, une limite de coût et ce que vous laissez de côté dans la première version.

Les questions à poser

  • Quel trafic normal et quel pic ?
  • Combien de temps dure le pic ?
  • Le service doit-il survivre à la perte d’une zone entière ?
  • Vers quelles destinations les VM doivent-elles sortir ?
  • Quel signal déclenche l’autoscaling ?
  • Comment protéger l’entrée HTTP contre des abus simples ?
Modèle de fiche de cadrage
Besoin ou questionRéponse ou hypothèsePreuve prévue
À compléterQui l’a confirmée ?Comment la vérifier ?
Avant de continuer

Montrez au formateur une fiche courte avec vos hypothèses et trois critères de réussite. Il valide le périmètre et le plafond de charge avant la suite.

02 Architecture

Comprendre l’architecture cible

Cette architecture est votre point de départ. Votre travail consiste à la déployer avec Terraform, à la sécuriser, à l’automatiser et à vérifier qu’elle se comporte comme prévu.

L’entrée HTTP passe par le Load Balancer, les sorties des VM passent par NAT, et le MIG régional répartit les instances. Ne confondez pas le health check qui oriente le trafic avec la politique d’autohealing qui remplace une VM malade.

flowchart TB
 U["Visiteur"] --> LB["Application Load Balancer externe"]
 ARM["Cloud Armor
policy sur le backend"] -.-> LB
 LB --> BE["Backend service
et health check"]
 BE --> A["VM privée
zone A"]
 BE --> B["VM privée
zone B"]
 MIG["MIG régional
template et autoscaler"] -.-> A
 MIG -.-> B
 A --> NAT["Cloud NAT
et Cloud Router"]
 B --> NAT
 NAT --> EXT["Dépendances externes"]
 LB -.-> OBS["Monitoring et logs"]
 MIG -.-> OBS

Sur un petit écran, faites défiler le schéma horizontalement.

Voir et copier le code Mermaid
flowchart TB
 U["Visiteur"] --> LB["Application Load Balancer externe"]
 ARM["Cloud Armor
policy sur le backend"] -.-> LB
 LB --> BE["Backend service
et health check"]
 BE --> A["VM privée
zone A"]
 BE --> B["VM privée
zone B"]
 MIG["MIG régional
template et autoscaler"] -.-> A
 MIG -.-> B
 A --> NAT["Cloud NAT
et Cloud Router"]
 B --> NAT
 NAT --> EXT["Dépendances externes"]
 LB -.-> OBS["Monitoring et logs"]
 MIG -.-> OBS

Comment ça circule

Le visiteur appelle le point d’entrée

Le Load Balancer reçoit la requête et la répartit entre les backends disponibles, selon sa configuration.

Les VM servent la page

Le MIG crée les instances à partir du même template, dans plusieurs zones. Aucune VM applicative n’a d’IP publique.

La capacité évolue

L’autoscaler ajuste la taille du groupe selon le signal et les bornes choisis. Tenez compte du temps de démarrage, des health checks et du plafond.

Les sorties passent par NAT

Cloud NAT donne aux VM privées les accès sortants dont elles ont besoin. Ce chemin n’a rien à voir avec le trafic entrant, qui arrive par le Load Balancer.

À retenir

Répartir les VM entre zones, remplacer une VM supprimée et ajouter de la capacité sont trois mécanismes différents. Prévoyez une preuve pour chacun.

Approfondir dans la documentation Google Cloud

À faire à partir du schéma

  • Suivez une requête ou un événement de bout en bout et expliquez le rôle de chaque service GCP.
  • Listez les ressources Terraform nécessaires, leurs dépendances et ce que chaque module expose aux autres.
  • Repérez ce qui reste à décider : région et zones, exposition réseau, identités et permissions, dimensionnement, seuils et politique de reprise.
  • Annotez trois choix d’infrastructure avec leurs compromis. Gardez un schéma fidèle à ce que vous déployez réellement.
Avant de continuer

Présentez au formateur votre découpage Terraform, les flux autorisés et les paramètres retenus. On parle ici d’infrastructure : concevoir une application métier n’est pas l’objet du TP.

03 Construire

Construire le premier parcours

Avancez par petits pas : vous déployez, vous vérifiez, puis vous ajoutez la dépendance suivante. Les ressources finales sont décrites dans Terraform. La console reste utile pour observer et diagnostiquer.

À faire

  1. Créez le réseau, les sorties nécessaires et les règles de pare-feu que votre schéma justifie.
  2. Construisez un template reproductible et un MIG régional d’au moins deux instances réparties sur deux zones, si le budget validé le permet.
  3. Configurez le Load Balancer, ses backends et ses sondes, puis montrez que plusieurs instances répondent.
  4. Configurez le remplacement des instances défaillantes et l’autoscaling, en documentant le minimum, le maximum, le signal et les délais.

À montrer

Des réponses venant de plusieurs instances, des VM sans IP externe et un plan Terraform sans modification inattendue.

À expliquer

Une sonde du Load Balancer détecte une application en panne. Est-ce que cela suffit à remplacer la VM ? Où avez-vous configuré ce comportement ?

Besoin d’un indice ?

Indice 1, une piste

Séparez trois décisions : où envoyer une requête, quand remplacer une VM et quand ajouter de la capacité.

Indice 2, où chercher

Health checks et autohealing du MIG

Les ressources du provider sont ensuite listées dans la rubrique Documentation Terraform.

Indice 3, un coup de pouce

Le health check du Load Balancer et la politique d’autohealing du MIG ont des rôles distincts. Vérifiez les délais de démarrage avant de provoquer une panne : une sonde trop agressive peut entretenir une boucle de remplacements.

Organiser Terraform

Découpez vos modules par responsabilité. Écrivez d’abord leurs entrées, leurs sorties et leurs dépendances : le module racine se contente de les assembler. Chaque module a une courte documentation. Inutile en revanche de créer un module par ressource.

Un découpage à discuter, une fois votre propre proposition faite

Chaque module contient main.tf, variables.tf, outputs.tf et un court README. Le module racine relie les sorties des uns aux entrées des autres. Ce découpage est une proposition : à vous de le défendre ou de l’améliorer.

ModuleResponsabilitéEntrées principalesSorties utiles
networkLe VPC, le subnet, Cloud Router et NAT, et les règles de pare-feu.region, cidr, règles pour le LB et les health checksnetwork_id, subnet_id, router_id
web_fleetL’Instance Template, le MIG régional, l’autoscaler et l’autohealing.subnet_id, image, zones, min et max, signalinstance_group, named_port
edgeLe Load Balancer, le backend service, son health check et Cloud Armor.instance_group, named_port, règles de protectionip_address, backend_service_id
observabilityLe dashboard du LB, du MIG et des VM, et les alertes de latence et de capacité.backend_id, mig_id, seuils, canauxdashboard_id, alert_policy_ids

Une organisation de dépôt possible :

infra/
  bootstrap/          # backend GCS et accès CI, cycle séparé
  envs/
    lab/              # module racine déployé, backend et variables
    prod-design/      # variante documentée, sans déploiement imposé
  modules/
    network/
    web_fleet/
    edge/
    observability/
app/                  # le kit de démarrage, si vous l’utilisez
tests/
  smoke/              # vérification du chemin nominal
  load/               # scripts et jeux synthétiques
docs/
  architecture.mmd
  decisions/          # décisions et alternatives (ADR)
  runbooks/           # diagnostic et reprise
  evidence/           # résultats, sans secret
README.md

État, environnements et bootstrap

L’état Terraform vit dans un bucket GCS créé par un bootstrap séparé, lancé avant le laboratoire et qui n’en dépend pas. Activez le versioning de ce bucket et limitez qui peut y accéder. Le backend GCS gère le verrouillage, et un préfixe par environnement évite de mélanger les états.

Ne commitez ni l’état, ni les plans, ni les clés, ni les valeurs de secrets. Attention, sensitive = true masque une valeur à l’affichage mais ne la retire pas du state. Gardez plutôt un fichier d’exemple avec les variables non sensibles. Voir la documentation des modules Terraform.

Avant de continuer

Le chemin nominal fonctionne. terraform fmt -check et terraform validate passent, et un nouveau plan ne propose rien d’inattendu. Un autre membre du groupe sait expliquer les dépendances.

04 Automatiser

Rendre le déploiement reproductible

Un collègue doit pouvoir relire puis déployer un changement sans refaire vos manipulations dans la console.

À faire

  • À chaque changement, la CI vérifie le format, initialise sans backend et lance validate. Les versions sont figées et le fichier de verrouillage est commité.
  • La CI s’authentifie par fédération d’identité, limitée à votre dépôt et aux branches ou environnements autorisés. Aucune clé JSON durable dans Git.
  • Le plan est produit sur une branche de confiance, relu, puis appliqué tel quel après une approbation explicite. Un seul déploiement à la fois par environnement.
  • Un test du parcours nominal suit l’apply. Si quelque chose échoue, le pipeline s’arrête là.
  • La destruction est un job manuel et approuvé, suivi d’un inventaire des ressources restantes. Le bootstrap garde son propre cycle.

À montrer

Un petit changement suivi de son commit jusqu’au test final, et une erreur de validation volontaire arrêtée avant tout apply. Les plans sont des artefacts privés, conservés peu de temps.

Besoin d’un indice ?

Indice 1, une piste

Dessinez les jobs et demandez-vous quelle identité exécute chacun d’eux. Qu’est-ce qui prouve que le plan appliqué est bien celui qui a été relu ?

Indice 2, où chercher

La page du backend GCS et celle sur la fédération d’identité pour les pipelines.

Indice 3, un coup de pouce

Séparez quatre temps : une validation sans accès au cloud, un plan authentifié, une approbation, puis l’apply. Rattachez le plan au commit et à l’environnement. Si le code, les variables ou l’état changent, le plan doit être recalculé et relu à nouveau.

Avant de continuer

Montrez une exécution de la CI avec son test final. Une procédure manuelle documentée aide au diagnostic, mais elle ne remplace pas cette preuve d’automatisation.

05 Éprouver

Observer, tester, expliquer

Annoncez votre hypothèse avant chaque test. Un graphique doit répondre à une question précise : avoir un dashboard ne prouve pas, à lui seul, que le système est fiable.

Préparer les mesures

La latence p95, les erreurs, le nombre de backends sains, la taille réelle et souhaitée du MIG, et le signal choisi pour l’autoscaling.

Déployez avec Terraform un dashboard et deux alertes utiles. Pour chacune, précisez la métrique, le filtre, la fenêtre, le seuil, le destinataire et la première action de diagnostic. Vérifiez que la notification arrive bien, puis qu’elle se referme quand tout revient à la normale.

Des signaux à adapter
SignalOù le trouverDécision ou exemple de seuil
Latence p95 et taux de 5xxLoad BalancerExemple pour le labo : un p95 au-dessus de 1 s pendant 3 minutes, avec un trafic suffisant. Regardez alors les backends.
Instances saines, taille souhaitée et réelleMIG et health checksRepérez un écart qui dure au-delà du temps de démarrage, et examinez le script de démarrage.
Signal de capacité et ressources des VMCPU, métrique du LB ou métrique applicativeVérifiez que le signal suit bien la saturation, et gardez une trace de chaque changement de capacité.

L’essai de charge du laboratoire

Mesurez 5 requêtes par seconde pendant 2 minutes, puis 5, 10 et 20 requêtes par seconde pendant 1 minute chacune. Ne dépassez pas la capacité maximale convenue. Si aucun scale-out ne se produit, expliquez pourquoi plutôt que d’augmenter la charge sans limite.

Avant de lancer quoi que ce soit, fixez la cible autorisée, le plafond de ressources, la durée, le volume et la règle d’arrêt. Arrêtez si le coût dérape ou si la dégradation persiste. Un exemple de seuil à faire valider : plus de 5 % d’erreurs pendant 30 secondes.

Une expérience pour vous entraîner

Sur une seule VM du MIG de laboratoire, arrêtez le service HTTP. Suivez son retrait du trafic, les erreurs vues par le client et son éventuel remplacement, puis comparez la durée observée aux délais configurés.

Écrivez la procédure de retour avant de commencer et ne changez qu’un paramètre à la fois. Si vous modifiez quelque chose à la main pour diagnostiquer, remettez ensuite Terraform en accord avec l’état voulu.

À montrer

Une chronologie de la panne et du retour au service, les réponses vues par le client et les courbes de capacité. Une instance remplacée n’est pas une décision de scale-out : ne confondez pas les deux.

Trame du compte rendu
  • L’hypothèse et le résultat attendu.
  • Le périmètre, la charge, l’heure de début et l’heure de fin.
  • Ce que vous avez observé avant, pendant et après, et l’impact sur le parcours utilisateur.
  • Les pistes de diagnostic, les vérifications faites et la cause retenue.
  • La correction, la preuve du retour au service et les limites de votre conclusion.
Une nouvelle contrainte

En cours de route, le formateur vous annoncera un événement supplémentaire. Vous devrez en expliquer l’impact et proposer une réponse. Selon le temps et le budget, cette réponse pourra mêler une expérience bornée et une évolution d’architecture argumentée.

Avant de continuer

Votre compte rendu sépare ce qui a été mesuré, ce qui reste une hypothèse et ce qui demanderait un test plus large. Chaque membre sait lire les courbes.

06 Présenter

Présenter ce que vous avez réalisé

Chaque groupe dispose de 20 minutes de présentation technique, démonstration comprise. Les trois membres prennent la parole. Montrez la solution telle qu’elle est, avec ses résultats et ses limites, et gardez des traces de secours au cas où la démonstration en direct échouerait.

SéquenceDuréeÀ montrer
Besoin et périmètre2 minLes hypothèses, les objectifs et le minimum réellement livré.
Architecture réalisée4 minLe schéma, les flux, la sécurité, vos choix et les alternatives écartées.
Terraform et pipeline4 minLes modules et leurs interfaces, l’état distant et la trace d’un déploiement par la CI.
Démonstration et résultats6 minLe chemin nominal, l’essai de charge, l’incident, le monitoring et la reprise. Prévoyez des captures au cas où la démonstration échouerait.
Coûts et limites3 minLes coûts estimés, les écarts avec la production, ce qui n’est pas fait et la suite.
Conclusion1 minLe bilan technique et ce que le groupe retient.

Ce que votre support doit référencer

  • Le dépôt, son README de déploiement et de destruction, le contrat de chaque module et une trace du pipeline.
  • Le schéma réellement déployé et trois décisions argumentées.
  • Les résultats de charge, le compte rendu d’incident, les alertes et le runbook.
  • Une estimation des principaux coûts, ce qui n’a pas été réalisé et l’écart avec une cible de production.

Après la présentation

À la date convenue avec le formateur, détruisez les ressources du laboratoire, vérifiez ce qui reste et signalez ce que vous gardez volontairement. Tenir le budget et nettoyer font partie du travail.

Avant la présentation

Répétez avec un chronomètre. Chaque membre doit pouvoir expliquer un flux, une permission, une panne et une limite sans se contenter de lire les diapositives.

Où en êtes-vous ?

Socle

Version complète

Ces cases sont enregistrées dans votre navigateur uniquement. Le formateur ne les voit pas.

Si vous êtes en avance

Choisissez une seule extension, une fois le parcours principal terminé et son coût validé. Ajouter des services ne remplace pas des preuves de qualité.

  • Tester une mise à jour progressive et le retour à une version saine.
  • Ajouter une règle Cloud Armor sur un chemin de démonstration et vérifier son effet.
  • Comparer le CPU à un signal plus proche de la saturation réelle de votre application.

Documentation Terraform

Ouvrez ces aides quand vous en avez besoin. Le rôle de chaque service est indiqué, mais leur assemblage, leurs paramètres et leurs permissions restent à concevoir.

Ressources Terraform utiles

Cette liste est un point de départ et votre architecture demandera d’autres ressources. Chaque lien ouvre la page du provider Google, avec ses arguments et ses exemples.

RessourceRôleConseil
google_compute_instance_templateDécrire les VM et leur démarrage.Pas de bloc access_config si les VM ne doivent pas avoir d’IP externe. Le script de démarrage doit pouvoir être rejoué sans casse.
google_compute_region_instance_group_managerCréer le MIG réparti sur plusieurs zones.Régional, pas zonal. Exposez le groupe d’instances et le named port au module du Load Balancer.
google_compute_region_autoscalerDéfinir les bornes et le signal de capacité.Commencez avec un plafond bas, et tenez compte du temps de démarrage et du signal réellement mesuré.
google_compute_health_checkVérifier que le service répond.Le port et le chemin doivent exister dans l’application. Distinguez le contrôle du LB de celui de l’autohealing.
google_compute_backend_serviceAssocier le MIG au Load Balancer.Vérifiez le named port, les health checks et la capacité déclarée du backend.
google_compute_router_natPermettre les sorties des VM privées.Il faut un Cloud Router. NAT sert aux sorties, pas à exposer le site.
google_compute_firewallN’autoriser que les flux nécessaires.Les sources à autoriser dépendent du type de Load Balancer retenu : reportez-vous à sa documentation.
google_compute_security_policyAjouter une policy Cloud Armor.Commencez par une règle simple, que vous pouvez tester sans bloquer toute la démonstration.
Ressources communes et configuration du provider

google_project_service active les API nécessaires, et google_service_account crée une identité dédiée. Donnez à chaque identité les permissions dont elle a besoin, au bon niveau, plutôt qu’un rôle Owner ou Editor sur tout le projet.

Pour l’observabilité, il vous faudra un dashboard, une politique d’alerte et un canal de notification. Vérifiez que la notification arrive réellement.

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "8.6.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

Cette version est un exemple, vérifié lors de la préparation du support. Contrôlez la dernière version publiée avant le TP. Déclarez vos variables et commitez .terraform.lock.hcl. Si votre projet utilise déjà une autre version, n’en changez pas sans relire le plan et les notes de migration. En local, utilisez ADC ; en CI, une identité fédérée. Jamais de clé JSON dans le code.

Documentation du provider Google

Critères d’évaluation

L’évaluation porte sur ce que le groupe a réalisé, sur ses résultats et sur la façon dont il les explique. Il n’y a pas de barème chiffré.

CritèreCe qui doit être observable
Cadrage et architectureDes hypothèses explicites, des flux lisibles, des alternatives et des compromis documentés.
Modules et reproductibilitéDes responsabilités cohérentes, des interfaces claires, un état distant, des versions figées et un redéploiement documenté.
Pipeline TerraformUne validation automatique, un plan relu puis appliqué tel quel, et des identités CI limitées.
Réseau, IAM et secretsDes expositions justifiées, le moindre privilège, aucun secret dans Git et un state protégé.
Charge et résilienceUn protocole reproductible, des mesures avant et après, un incident analysé et un retour au service démontré.
ObservabilitéUn dashboard utile, des seuils justifiés, une notification vérifiée et une procédure de diagnostic.
Coût et périmètreDes ordres de grandeur, les limites du laboratoire, la destruction et le contrôle des ressources restantes.
Présentation et défense collectiveLe groupe tient ses 20 minutes et montre ce qui fonctionne vraiment, avec ses limites. Chaque membre explique les choix, y compris le code proposé par un LLM.