Aller au contenu

Lab : Signalements sur Scaleway, brique par brique

200 Pratiquer ⏱ 4 h cloudscalewaycloud-inits3

Prérequis

Testé avec scaleway-cli 2.62.0 , vérifié le 5 octobre 2026

Ce lab rassemble le cours Le cloud : les fondamentaux. Vous construisez dans votre projet Scaleway l'architecture de l'application Signalements telle qu'elle est à la fin du cours, puis un script interroge l'API pour vérifier qu'elle a les propriétés attendues. Il valide le niveau 200 de la compétence « Fondamentaux du cloud », et contribue aux niveaux 200 de « Réseau cloud » et « Identité, authentification et IAM ».

Comptez une demi-journée, et quelques euros si vous supprimez tout à la fin (leçon 8). Toutes les pièces sont dans les leçons ; essayez d'écrire chaque commande avant d'ouvrir la leçon correspondante.

Note

Le script de vérification a été écrit à partir de l'aide de la CLI scw 2.62 et des structures de réponse du SDK Go de Scaleway, dont la CLI affiche les champs tels quels en sortie JSON. Il ne modifie rien : il ne fait que des lectures (list, get).

Ce qu'il faut

  • Un compte Scaleway et la CLI scw configurée sur le projet signalements (leçon 1).
  • jq, curl, un client SSH, et l'AWS CLI version 2 configurée pour l'Object Storage de Scaleway (leçon 5).
  • Un nom de domaine n'est pas nécessaire : le frontend HTTPS de la leçon 6 est facultatif dans ce lab.

Ce qu'il faut atteindre

    flowchart LR
  U["Internet"] -->|"HTTP 80"| LB["lb-signalements<br/>fr-par-1"]
  E["Votre adresse IP"] -->|"SSH 61000"| GW["gw-signalements<br/>NAT + bastion"]
  subgraph PN["pn-signalements"]
    S1["sig-app-1<br/>fr-par-1"]
    S2["sig-app-2<br/>fr-par-2"]
    DB[("sig-db<br/>point d'accès privé seul")]
  end
  LB -->|":8000, GET /sante"| S1
  LB --> S2
  GW -.-> S1
  GW -.-> S2
  S1 --> DB
  S2 --> DB
  B[("signalements-pj-&lt;suffixe&gt;<br/>bucket privé")]
  

Les exigences

ExigenceLeçon
Toutes les ressources sont dans le projet signalements, jamais dans default1, 7
sig-db est une base PostgreSQL managée, dont le seul point d'accès est sur pn-signalements2, 6
sig-app-1 tourne en fr-par-1, sig-app-2 en fr-par-2, toutes deux créées avec cloud-init, sans secret dans les données utilisateur3, 4
Les instances n'ont aucune adresse publique, et ont une interface sur pn-signalements6
Chaque instance a un groupe de sécurité à état dont la politique entrante par défaut est drop6
Le conteneur n'écoute que sur l'adresse privée ; la base est configurée par /etc/signalements/env, pas par cloud-init4, 6
gw-signalements fournit la sortie NAT et un bastion dont les adresses autorisées ne contiennent pas 0.0.0.0/06
lb-signalements répartit sur les deux instances, avec une vérification de santé HTTP sur /sante6
Le bucket signalements-pj-<suffixe> refuse les lectures anonymes ; le versionnement est activé5
L'application IAM sig-deploiement n'a que des règles limitées au projet, et toutes ses clés expirent7
Aucune IP flexible n'est réservée sans être attachée8
Un budget avec alerte existe sur l'organisation8

Étape 1 : le socle

Créez le projet s'il n'existe plus, puis la base (leçon 2) et les deux instances (leçons 3 et 4). Gardez le fichier signalements.yaml de la leçon 4 : il ne doit contenir aucun mot de passe. Vérifiez cloud-init sur chaque instance avec cloud-init status --wait avant de continuer.

Étape 2 : le réseau

Suivez la leçon 6 dans l'ordre, sans en sauter : VPC et réseau privé, interfaces privées, publication du conteneur sur l'adresse privée, point d'accès privé de la base puis bascule de DATABASE_URL puis suppression du point d'accès public, passerelle avec NAT et bastion, vérification de la sortie depuis les instances, et seulement ensuite le détachement et la suppression des IP publiques. Restreignez les adresses autorisées du bastion à la vôtre dans la console. Créez enfin le répartiteur, son backend et un frontend sur le port 80.

Testez la résistance à la panne d'une zone : arrêtez le service signalements sur sig-app-1 (par le bastion), et vérifiez que curl http://<IP du répartiteur>/sante répond toujours, puis relancez-le.

Étape 3 : le stockage et les identités

Créez le bucket, activez le versionnement, déposez une photo d'essai sous photos/, et générez une URL présignée de quinze minutes (leçon 5). Créez ensuite l'application sig-deploiement, sa politique et une clé qui expire (leçon 7), et vérifiez par un refus qu'elle ne peut pas lister les instances. Créez enfin un budget mensuel avec une alerte (leçon 8).

Vérifier

Enregistrez le script sous verifier-lab.sh, remplacez la valeur de BUCKET, et lancez-le avec votre profil scw habituel.

#!/usr/bin/env bash
# Vérifie les propriétés attendues du lab « Signalements sur Scaleway ».
# Usage : BUCKET=signalements-pj-xxxx ./verifier-lab.sh   (exige scw et jq)
# Chaque contrôle affiche OK ou ÉCHEC ; le code de sortie est le nombre d'échecs.
set -uo pipefail
echecs=0
BUCKET=${BUCKET:?définissez BUCKET=signalements-pj-<suffixe>}

controle() {   # controle "libellé" commande...
  local libelle=$1; shift
  if "$@" > /dev/null 2>&1; then
    echo "OK     $libelle"
  else
    echo "ÉCHEC  $libelle"
    echecs=$((echecs + 1))
  fi
}

PROJET=$(scw account project list -o json | jq -r '.[] | select(.name == "signalements") | .id')
PN=$(scw vpc private-network list region=fr-par project-id="$PROJET" -o json \
  | jq -r '.[] | select(.name == "pn-signalements") | .id')
export PROJET PN

controle "le projet signalements existe" test -n "$PROJET"
controle "le réseau privé pn-signalements existe dans le projet" test -n "$PN"

for paire in sig-app-1:fr-par-1 sig-app-2:fr-par-2; do
  nom=${paire%%:*}; zone=${paire##*:}
  srv=$(scw instance server list zone="$zone" project-id="$PROJET" -o json \
    | jq -c --arg n "$nom" '[.[] | select(.name == $n)][0] // empty')
  controle "$nom existe en $zone et tourne" \
    bash -c 'jq -e ".state == \"running\"" <<< "$1"' _ "$srv"
  controle "$nom n'a aucune adresse publique" \
    bash -c 'jq -e "(.public_ips // []) | length == 0" <<< "$1"' _ "$srv"
  controle "$nom a une interface sur pn-signalements" \
    bash -c 'jq -e --arg pn "$PN" "[.private_nics[]?.private_network_id] | index(\$pn) != null" <<< "$1"' _ "$srv"
  sg=$(jq -r '.security_group.id // empty' <<< "$srv")
  controle "$nom : groupe de sécurité à état, entrée refusée par défaut" \
    bash -c 'scw instance security-group get "$1" zone="$2" -o json \
      | jq -e "(.security_group // .) | .stateful == true and .inbound_default_policy == \"drop\""' _ "$sg" "$zone"
  controle "$nom : aucune adresse IP flexible orpheline en $zone" \
    bash -c 'scw instance ip list zone="$1" project-id="$2" -o json | jq -e "[.[] | select(.server == null)] | length == 0"' _ "$zone" "$PROJET"
done

db=$(scw rdb instance list region=fr-par project-id="$PROJET" -o json \
  | jq -c '[.[] | select(.name == "sig-db")][0] // empty')
controle "sig-db existe et est prête" \
  bash -c 'jq -e ".status == \"ready\" and (.engine | startswith(\"PostgreSQL\"))" <<< "$1"' _ "$db"
controle "sig-db n'a plus de point d'accès public" \
  bash -c 'jq -e "[.endpoints[] | select(.load_balancer != null)] | length == 0" <<< "$1"' _ "$db"
controle "sig-db a un point d'accès sur pn-signalements" \
  bash -c 'jq -e --arg pn "$PN" "[.endpoints[].private_network.private_network_id] | index(\$pn) != null" <<< "$1"' _ "$db"

gw=$(scw vpc-gw gateway list zone=fr-par-1 project-id="$PROJET" -o json \
  | jq -c '[.[] | select(.name == "gw-signalements")][0] // empty')
controle "gw-signalements a son bastion activé" \
  bash -c 'jq -e ".bastion_enabled == true" <<< "$1"' _ "$gw"
controle "le bastion n'accepte pas tout Internet" \
  bash -c 'jq -e "(.bastion_allowed_ips // []) | length > 0 and (index(\"0.0.0.0/0\") == null)" <<< "$1"' _ "$gw"
controle "gw-signalements annonce la route par défaut sur le réseau privé" \
  bash -c 'jq -e --arg pn "$PN" "[.gateway_networks[] | select(.private_network_id == \$pn and .push_default_route == true)] | length == 1" <<< "$1"' _ "$gw"

lb=$(scw lb lb list zone=fr-par-1 project-id="$PROJET" -o json \
  | jq -r '.[] | select(.name == "lb-signalements") | .id')
controle "lb-signalements existe" test -n "$lb"
be=$(scw lb backend list lb-id="$lb" zone=fr-par-1 -o json 2>/dev/null | jq -c '.[0] // empty')
controle "le backend contient les deux instances" \
  bash -c 'jq -e ".pool | length == 2" <<< "$1"' _ "$be"
controle "la vérification de santé interroge /sante en HTTP" \
  bash -c 'jq -e ".health_check.http_config.uri == \"/sante\"" <<< "$1"' _ "$be"

controle "le bucket refuse la liste anonyme" \
  bash -c 'code=$(curl -s -o /dev/null -w "%{http_code}" "https://$1.s3.fr-par.scw.cloud/"); [ "$code" = 403 ]' _ "$BUCKET"
controle "le versionnement du bucket est activé" \
  bash -c 'aws s3api get-bucket-versioning --bucket "$1" --output json | jq -e ".Status == \"Enabled\""' _ "$BUCKET"

app=$(scw iam application list -o json | jq -r '.[] | select(.name == "sig-deploiement") | .id')
controle "l'application sig-deploiement existe" test -n "$app"
regles=$(for p in $(scw iam policy list application-ids.0="$app" -o json | jq -r '.[].id'); do
  scw iam rule list "$p" -o json; done | jq -s 'add // []')
controle "sig-deploiement a au moins une règle" \
  bash -c 'jq -e "length > 0" <<< "$1"' _ "$regles"
controle "toutes ses règles sont limitées au projet signalements" \
  bash -c 'jq -e --arg p "$PROJET" "all(.project_ids == [\$p] and (.organization_id // null) == null)" <<< "$1"' _ "$regles"
controle "toutes ses clés d'API expirent" \
  bash -c 'scw iam api-key list bearer-id="$1" -o json | jq -e "length > 0 and all(.expires_at != null)"' _ "$app"

controle "un budget existe sur l'organisation" \
  bash -c 'scw billing budget list -o json | jq -e "length > 0"'

echo
echo "$echecs échec(s)"
exit "$echecs"

Quelques contrôles demandent une explication :

  • Les filtres name= des commandes list cherchent une sous-chaîne : sig-app-1 trouverait aussi sig-app-1b. Le script filtre donc le nom exact avec jq.
  • Le point d'accès public de la base est, dans la réponse de l'API, un point d'accès qui porte un champ load_balancer ; un point d'accès privé porte un champ private_network.
  • La liste anonyme du bucket est un GET sans signature sur l'adresse du bucket. Un bucket privé répond 403 ; un 200 signifie que n'importe qui peut lister vos fichiers.
  • Les règles IAM se lisent politique par politique avec scw iam rule list. Une règle qui porte un organization_id s'applique à toute l'organisation, ce que le lab interdit pour un pipeline.

Les exigences sur cloud-init (pas de secret dans les données utilisateur, conteneur sur l'adresse privée) ne se vérifient pas par l'API de façon fiable : contrôlez-les à la main, par le bastion, avec curl --local-port 1-1023 http://169.254.42.42/user_data/cloud-init (aucun mot de passe ne doit apparaître) et ss -ltnp (le port 8000 ne doit écouter que sur l'adresse en 172.16.20.x).

Démonter

Supprimez tout dans l'ordre des dépendances de la leçon 8 (répartiteur, passerelle, instances avec leurs volumes et leurs IP, réseau privé et VPC, bucket vidé de toutes ses versions, base et ses sauvegardes, clés et applications IAM), puis relancez le script d'inventaire de la leçon 8 : il ne doit plus rien lister. Revenez sur la page de consommation le lendemain pour vérifier que plus rien ne s'accumule.

Plan du cours