Lab : Signalements sur Scaleway, brique par brique
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
scwconfigurée sur le projetsignalements(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-<suffixe><br/>bucket privé")]
Les exigences
| Exigence | Leçon |
|---|---|
Toutes les ressources sont dans le projet signalements, jamais dans default | 1, 7 |
sig-db est une base PostgreSQL managée, dont le seul point d'accès est sur pn-signalements | 2, 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 utilisateur | 3, 4 |
Les instances n'ont aucune adresse publique, et ont une interface sur pn-signalements | 6 |
Chaque instance a un groupe de sécurité à état dont la politique entrante par défaut est drop | 6 |
Le conteneur n'écoute que sur l'adresse privée ; la base est configurée par /etc/signalements/env, pas par cloud-init | 4, 6 |
gw-signalements fournit la sortie NAT et un bastion dont les adresses autorisées ne contiennent pas 0.0.0.0/0 | 6 |
lb-signalements répartit sur les deux instances, avec une vérification de santé HTTP sur /sante | 6 |
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 expirent | 7 |
| Aucune IP flexible n'est réservée sans être attachée | 8 |
| Un budget avec alerte existe sur l'organisation | 8 |
É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 commandeslistcherchent une sous-chaîne :sig-app-1trouverait aussisig-app-1b. Le script filtre donc le nom exact avecjq. - 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 champprivate_network. - La liste anonyme du bucket est un
GETsans signature sur l'adresse du bucket. Un bucket privé répond403; un200signifie 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 unorganization_ids'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.