Aller au contenu
Le calcul : instances, images et cloud-init

Le calcul : instances, images et cloud-init

200 Pratiquer ⏱ 1 h 15 cloudscalewaycloud-initdocker

À la fin, vous saurez

  • Choisir un type d'instance en distinguant vCPU partagés et dédiés, stockage local et stockage bloc
  • Créer une instance par la ligne de commande avec une image, une clé SSH et des données utilisateur cloud-init
  • Écrire, valider et déboguer une configuration cloud-config qui installe et lance une application conteneurisée
  • Interroger le service de métadonnées depuis l'instance et expliquer pourquoi il est une cible pour un attaquant
  • Dire ce que coûte une instance dans chacun de ses états (en marche, en veille, arrêtée, supprimée)
  • Expliquer la différence entre réparer un serveur et le remplacer, et ce qu'elle change pour l'exploitation

Prérequis

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

Pourquoi

Le serveur sur lequel tourne aujourd'hui Signalements a été installé il y a trois ans par une personne qui a quitté l'équipe. Il fonctionne. Personne ne sait exactement ce qu'il contient : un paquet ajouté un soir d'incident, un fichier de configuration modifié à la main, une tâche planifiée dont on a oublié l'objet. Le jour où il tombera, il faudra le reconstruire de mémoire, et l'on découvrira ce qu'il faisait en constatant ce qui ne marche plus.

Le cloud ne règle pas ce problème tout seul. On peut parfaitement louer une machine virtuelle chez Scaleway et l'administrer exactement comme ce vieux serveur : s'y connecter, installer, bricoler. On aura alors le même serveur unique et irremplaçable, simplement facturé à l'heure.

Ce que le cloud rend possible, c'est autre chose : une machine qui se construit seule, à partir d'une description écrite, en quelques minutes, autant de fois qu'on le veut. Si elle tombe, on en crée une autre, identique. Si on veut la modifier, on modifie la description et l'on remplace la machine. Cette leçon montre les trois pièces qui le permettent : l'instance (la machine louée), l'image (son point de départ) et cloud-init (l'outil qui la configure au premier démarrage, à partir de données que vous lui fournissez). À la fin, sig-app-1 et sig-app-2 tournent dans les deux zones choisies à la leçon précédente, et aucune des deux n'a été touchée à la main.

Les concepts

Une instance est une machine virtuelle

Une instance est une machine virtuelle louée à un fournisseur : un système d'exploitation complet, avec ses processeurs virtuels, sa mémoire, ses disques et ses interfaces réseau, qui tourne sur un serveur physique partagé avec d'autres clients. Ce serveur physique exécute un hyperviseur, le logiciel qui découpe ses ressources réelles (cœurs, mémoire, disques, réseau) en machines virtuelles isolées les unes des autres. Le mot « instance » vient de là : chaque machine est une instance d'un type décrit au catalogue.

Chaque fournisseur a son nom pour la même chose :

ScalewayAWSAzureGoogle CloudOVHcloud
InstanceEC2 instanceVirtual MachineCompute Engine VMInstance Public Cloud (OpenStack)
ImageAMIImage (Compute Gallery)ImageImage
Cloud-init (données utilisateur)User dataCustom data, user dataMétadonnée user-data ou startup-scriptUser data
Groupe d'autoscaling (bêta)Auto Scaling groupVirtual Machine Scale SetManaged instance group(via les outils OpenStack)

Choisir un type d'instance

Un type d'instance fixe quatre choses : le nombre de processeurs virtuels (vCPU), la quantité de mémoire, le type de stockage accepté et la bande passante réseau maximale. Le catalogue de Scaleway les regroupe en gammes, dont la documentation donne la liste complète dans sa fiche technique (Instances datasheet). Au moment de la rédaction :

GammeFamillesvCPUPour quoi
DevelopmentDEV1, GP1, PLAY2, STARDUST1partagésDéveloppement, tests, petits services
General PurposeBASIC3, BASIC2, PRO2 (partagés) ; STANDARD3, STANDARD2, POP2 (dédiés)partagés ou dédiésProduction, de la petite API à la base de données chargée
SpecializedCOMPUTE3, MEMORY3, POP2-HC, POP2-HMdédiésCalcul intensif, mémoire abondante
GPUL4, H100, B300...dédiésApprentissage automatique, inférence

Les noms suivent souvent la forme FAMILLE-<vCPU>C-<mémoire>G : STANDARD3-X2C-8G a 2 vCPU et 8 Gio de mémoire. Les familles plus anciennes ont des tailles nommées (PRO2-XXS, PRO2-S, DEV1-M). Les générations se succèdent : la documentation présente les STANDARD3 (processeurs AMD EPYC de génération Turin) comme jusqu'à 35 % plus performantes que les POP2. Scaleway documente aussi un cycle de vie des types : « fin de nouvelles fonctionnalités », « fin de commercialisation », puis « fin de vie », où les instances restantes sont arrêtées et migrées vers une offre équivalente. Un type choisi aujourd'hui sera à remplacer dans quelques années ; c'est un argument de plus pour des instances que l'on sait recréer.

vCPU partagés ou dédiés. C'est le choix qui compte le plus après la taille. Sur une instance à vCPU dédiés, chaque vCPU correspond à un fil d'exécution physique réservé : les performances sont constantes. Sur une instance à vCPU partagés, vos vCPU sont ordonnancés sur des cœurs physiques que se partagent plusieurs instances ; quand les voisins sont très actifs, votre instance attend son tour. Ce temps d'attente se voit dans Linux : c'est la colonne st (steal, temps volé) de top ou de vmstat. La documentation de Scaleway résume l'arbitrage : partagés pour le développement, les environnements de préproduction et les petites productions qui tolèrent des variations ; dédiés pour la production critique et les applications sensibles à la latence. La mémoire, elle, est toujours réservée à l'instance.

Stockage local ou bloc. Une instance démarre sur un volume racine. Deux technologies existent chez Scaleway :

  • le stockage local est un disque SSD de l'hyperviseur lui-même : rapide, mais lié à la machine physique ; seules certaines gammes anciennes (DEV1, GP1) l'acceptent ;
  • le stockage bloc (Block Storage) est un volume attaché par le réseau, comme un disque virtuel que l'on peut détacher et rattacher ailleurs ; les gammes récentes ne démarrent que sur lui (boot-on-block).

La leçon 5 revient en détail sur le stockage. Retenez ici la conséquence pratique : avec un volume bloc, les données du disque survivent à l'arrêt de l'instance et ne dépendent pas d'un hyperviseur particulier.

Le choix de Signalements. L'API est légère : deux processus gunicorn, quelques requêtes par seconde. L'équipe choisit PRO2-XXS : 2 vCPU partagés (AMD EPYC 7543), 8 Gio de mémoire, stockage bloc, 350 Mbit/s de bande passante d'après la fiche technique, et la leçon 3 a vérifié qu'il est disponible en fr-par-1 et fr-par-2. C'est un choix de départ, pas un dogme : si les mesures montrent du temps volé gênant, on passera à des vCPU dédiés (STANDARD3-X2C-8G) en changeant une ligne. Notez aussi que la gamme PRO2 n'a aucun objectif de disponibilité dans le SLA des instances de Scaleway (leçon 8) : c'est acceptable ici parce que la disponibilité de Signalements vient de ses deux instances dans deux zones, pas de la promesse faite sur chacune ; pour un client qui exige un engagement contractuel par machine, choisissez une gamme couverte.

L'image

Une image de machine est le point de départ du volume racine : un système d'exploitation installé, prêt à démarrer. On en distingue deux sortes :

  • les images publiques fournies par le fournisseur (chez Scaleway, le catalogue, ou marketplace : Ubuntu, Debian, Rocky Linux, des applications préinstallées appelées InstantApps) ; on les désigne par un libellé stable, comme ubuntu_noble pour Ubuntu 24.04, que la CLI traduit en l'identifiant de la dernière version de l'image dans la zone visée ;
  • les images personnalisées, que vous fabriquez vous-même. Chez Scaleway, une image est une copie complète d'une instance, tous volumes compris (scw instance server backup), ou se construit à partir d'un instantané de volume. On les appelle souvent images dorées (golden images) quand elles servent de base standard à toute une organisation, durcies et à jour.

Toutes les images Linux fournies par Scaleway intègrent cloud-init, préinstallé et activé (la documentation excepte les images Windows).

L'accès : clés SSH et IP publique

Les images Linux de Scaleway s'utilisent avec le compte root, par clé SSH, sans mot de passe. Les clés publiques se déclarent dans l'IAM, au niveau du projet, et sont injectées dans les instances du projet : un script de l'image, scw-fetch-ssh-keys, réécrit le fichier /root/.ssh/authorized_keys à chaque démarrage à partir des métadonnées de l'instance. Le fichier commence d'ailleurs par un avertissement qui le dit. Deux conséquences : une clé ajoutée à la main dans ce fichier disparaît au redémarrage suivant, et une clé retirée du projet disparaît des instances au redémarrage suivant, pas immédiatement.

Pour être joignable depuis Internet, l'instance a besoin d'une adresse IP publique. Par défaut, scw instance server create en crée une (ip=new). Chez Scaleway, elle est flexible : elle appartient à votre projet, indépendamment de l'instance, survit à son arrêt et peut être déplacée d'une instance à l'autre. L'option ip=dynamic donne au contraire une adresse rendue au fournisseur à chaque arrêt ; ip=none, aucune adresse publique. À la leçon 6, les instances de Signalements perdront leur IP publique au profit d'un réseau privé, d'un répartiteur de charge et d'un bastion.

Le cycle de vie, et ce qu'il coûte

Une instance passe par plusieurs états, et chacun a un coût différent. Le tableau suivant résume la documentation Understanding Instance states de Scaleway et les pages sur l'arrêt et la veille :

ÉtatCe qui se passeCe qui est facturé
En marche (running)L'instance tourne sur un hyperviseurCalcul, stockage, IP
En veille (standby)Le système est arrêté, mais l'instance reste allouée sur son hyperviseur ; une commande poweroff depuis le système y mène aussiToujours facturée comme une instance, plus le stockage et les IP
Arrêtée (stopped, power off)L'hyperviseur est libéré ; le volume local éventuel est archivé, et restauré au redémarrageStockage (volumes bloc, archive du volume local) et IP flexibles, plus de calcul
SuppriméeL'instance et ses volumes locaux disparaissentPlus rien, sauf les IP flexibles et volumes que vous avez gardés

Le piège est dans la deuxième ligne. Un sudo poweroff dans l'instance ne l'« éteint » pas au sens de la facture : elle passe en veille et reste facturée. Pour arrêter la facturation du calcul, il faut l'arrêter par l'API (scw instance server stop), ce qui libère l'hyperviseur. Le redémarrage prend alors plus de temps, puisqu'il faut retrouver une place sur un hyperviseur, voire restaurer un volume local archivé, et peut même échouer si le type est en pénurie dans la zone.

Chez les autres fournisseurs, l'idée est la même avec d'autres règles : chez AWS, une instance EC2 arrêtée ne coûte plus que ses volumes EBS ; chez Azure, il faut désallouer une VM (et non seulement l'arrêter depuis le système) pour cesser de payer le calcul.

Le service de métadonnées

Une instance qui démarre a besoin de savoir qui elle est : son nom, son identifiant, ses adresses, les clés SSH autorisées, et la configuration que vous voulez lui appliquer. Elle l'apprend en interrogeant le service de métadonnées, un service HTTP que le fournisseur expose à chaque instance à une adresse lien-local (une adresse du bloc 169.254.0.0/16, joignable seulement depuis l'instance elle-même) :

FournisseurAdresseParticularité
Scalewayhttp://169.254.42.42 (et http://[fd00:42::42] en IPv6)Données utilisateur réservées aux ports source inférieurs à 1024
AWShttp://169.254.169.254IMDSv2 : jeton de session obtenu par un PUT
Azurehttp://169.254.169.254En-tête Metadata: true obligatoire
Google Cloudhttp://metadata.google.internal (169.254.169.254)En-tête Metadata-Flavor: Google obligatoire
OVHcloud (OpenStack)http://169.254.169.254Service de métadonnées d'OpenStack

Chez Scaleway, la documentation décrit trois catégories de données :

  • les métadonnées (meta-data) : la description de l'instance, comme si elle s'interrogeait elle-même par l'API (/conf, au format texte ou JSON) ;
  • les données du fournisseur (vendor-data) : une configuration que Scaleway applique par défaut ;
  • les données utilisateur (user-data) : ce que vous fournissez à la création de l'instance, et que cloud-init exécute.

cloud-init

cloud-init est l'outil standard, sur toutes les distributions Linux et chez presque tous les fournisseurs, qui configure une instance à son premier démarrage. Il détecte sur quel cloud il tourne, lit les métadonnées et les données utilisateur, puis applique la configuration : création d'utilisateurs, installation de paquets, écriture de fichiers, exécution de commandes.

Les données utilisateur acceptent plusieurs formats. Le plus courant, et le seul de cette leçon, est le cloud-config : un document YAML qui commence obligatoirement par la ligne #cloud-config, et dont chaque clé de premier niveau active un module de cloud-init (packages, users, write_files, runcmd...). Un script shell (commençant par #!) est aussi accepté, mais le cloud-config est déclaratif, validable et lisible. Chez Scaleway, seules les données en texte brut sont acceptées : pas de format compressé.

Point essentiel : la plupart des modules ne s'exécutent qu'au premier démarrage d'une instance donnée. cloud-init reconnaît ce premier démarrage à l'identifiant de l'instance : tant qu'il ne change pas, il considère que la configuration a déjà été appliquée. Modifier les données utilisateur d'une instance existante, puis la redémarrer, n'a donc en général aucun effet sur les modules marqués per-instance, ce que la documentation de Scaleway rappelle explicitement. cloud-init est un outil d'amorçage, pas un outil de gestion de configuration continue comme Ansible.

Animaux de compagnie et bétail

Cette propriété (on configure une fois, à la naissance) conduit à une façon de travailler que l'on résume par une image devenue célèbre : les serveurs animaux de compagnie ou bétail (pets vs cattle). Randy Bias, qui l'a popularisée vers 2011-2012 en reprenant une formule de Bill Baker, en donne ces définitions : les animaux de compagnie sont des serveurs (ou des paires de serveurs) traités comme indispensables, uniques, qui ne doivent jamais tomber ; le bétail, ce sont des ensembles de serveurs construits par des outils automatiques et conçus pour la panne, dont aucun n'est irremplaçable.

Un animal de compagnie malade, on le soigne : on s'y connecte, on diagnostique, on corrige. Une tête de bétail malade, on la remplace. Le vieux serveur de Signalements est un animal de compagnie. Les instances de cette leçon seront du bétail.

Chad Fowler a donné en 2013 un nom à la pratique qui en découle : l'infrastructure immuable (immutable infrastructure). Un serveur n'est jamais modifié après sa création ; tout changement (nouvelle version, correctif, configuration) passe par la création d'un nouveau serveur et la suppression de l'ancien. Son argument : si l'on sait qu'un système a été créé par automatisation et jamais modifié depuis, la plupart des problèmes de dérive disparaissent. C'est exactement le modèle des conteneurs, appliqué aux machines.

En pratique

Les commandes suivantes ont été vérifiées contre l'aide de scw 2.62 et la documentation de Scaleway ; elles créent des ressources facturées dans votre projet signalements. La leçon décrit les résultats sans reproduire de sortie. Supprimez les instances à la fin de la séance (dernière section) si vous ne poursuivez pas tout de suite avec la leçon 5.

Déclarer sa clé SSH

Si ce n'est pas déjà fait, déclarez votre clé publique dans le projet. Une clé Ed25519 est le bon choix aujourd'hui :

$ ssh-keygen -t ed25519 -C "camille@signalements"     # seulement si vous n'en avez pas
$ scw iam ssh-key create name=camille public-key=@"$HOME/.ssh/id_ed25519.pub"

La syntaxe @chemin demande à la CLI de lire la valeur dans un fichier. La clé est rattachée au projet par défaut de votre profil ; elle sera injectée dans toutes les instances de ce projet.

Écrire la configuration cloud-config

Créez un fichier signalements.yaml. Il décrit tout ce que la machine doit devenir :

#cloud-config
# Instance applicative Signalements : Docker et l'API, rien d'autre.

package_update: true
package_upgrade: true
packages:
  - docker.io

users:
  - name: exploit
    gecos: Compte d'exploitation nominatif partagé par l'astreinte
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... camille@signalements

write_files:
  - path: /etc/signalements/env
    permissions: "0640"
    content: |
      APP_VERSION=1.2.0
  - path: /etc/systemd/system/signalements.service
    permissions: "0644"
    content: |
      [Unit]
      Description=API Signalements (conteneur)
      Requires=docker.service
      After=docker.service network-online.target
      Wants=network-online.target

      [Service]
      ExecStartPre=-/usr/bin/docker rm -f signalements
      ExecStart=/usr/bin/docker run --rm --name signalements \
        --env-file /etc/signalements/env \
        -p 127.0.0.1:8000:8000 \
        ghcr.io/lyneko-formation/signalements:1.2.0
      ExecStop=/usr/bin/docker stop signalements
      Restart=always
      RestartSec=5

      [Install]
      WantedBy=multi-user.target

runcmd:
  - [systemctl, daemon-reload]
  - [systemctl, enable, --now, signalements.service]

Quelques choix méritent une explication :

  • docker.io plutôt que le dépôt de Docker. Le cours Docker : les fondamentaux compare les deux. Sur une instance qui ne fait que lancer un conteneur, le paquet de la distribution suffit : pas de dépôt tiers ni de clé à importer, des correctifs suivis par Ubuntu, et une configuration cloud-init de trois lignes. Si vous avez besoin des derniers plugins (Compose, Buildx), le module apt de cloud-init sait ajouter le dépôt officiel et sa clé ; la configuration est plus longue, et la version installée change au rythme de Docker.
  • package_upgrade: true applique les mises à jour de sécurité publiées depuis la fabrication de l'image. Une image publique a souvent quelques semaines ; sans cette ligne, l'instance démarre avec des failles déjà corrigées ailleurs. Le prix : un premier démarrage plus long de quelques minutes.
  • Un compte nominatif exploit. Il ne remplace pas l'accès root par les clés du projet (mécanisme de Scaleway, réécrit à chaque démarrage), mais il permet d'administrer avec sudo, donc avec une trace dans les journaux. Seule la clé publique figure dans le fichier : elle n'a rien de secret. lock_passwd: true interdit toute connexion par mot de passe.
  • Une unité systemd plutôt que docker run --restart always lancé une fois. systemd démarre le conteneur au démarrage de la machine, le relance s'il s'arrête, et ses journaux se lisent avec journalctl -u signalements. ExecStartPre=- (le tiret signifie « ignorer l'échec ») supprime un éventuel conteneur résiduel d'un arrêt brutal.
  • -p 127.0.0.1:8000:8000 publie le port uniquement sur l'interface locale. L'instance a une IP publique et le groupe de sécurité par défaut de la zone accepte le trafic entrant : publier sur 0.0.0.0 exposerait l'API à Internet, sans TLS, avant même que le répartiteur de charge existe. On testera par un tunnel SSH. La leçon 6 publiera le port sur l'interface du réseau privé.
  • Pas de DATABASE_URL. Sans elle, Signalements fonctionne en mémoire, ce qui suffit pour cette leçon. L'adresse de la base contient un mot de passe : elle ne doit pas figurer dans les données utilisateur (voir la section Sécurité). Elle sera fournie à l'instance par un autre canal quand la base sera raccordée au réseau privé.
  • runcmd sous forme de listes. Chaque élément est exécuté directement, sans passer par un shell : pas de surprise d'échappement.

Valider avant de créer

Une erreur de syntaxe dans un cloud-config ne se voit qu'après la création de l'instance, et parfois pas du tout : un module mal nommé est ignoré avec un simple avertissement dans les journaux. Validez donc le fichier avant. cloud-init schema le vérifie contre le schéma officiel ; si cloud-init n'est pas installé sur votre poste, un conteneur Ubuntu jetable fait l'affaire :

$ docker run --rm -v "$PWD":/w:ro ubuntu:24.04 sh -c \
    'apt-get update -qq && apt-get install -y -qq cloud-init >/dev/null \
     && cloud-init schema -c /w/signalements.yaml --annotate'
  • -c (ou --config-file) désigne le fichier à valider ;
  • --annotate réaffiche le fichier en signalant chaque erreur à côté de la ligne fautive.

Une configuration valide produit un message de succès ; une clé inconnue ou une valeur du mauvais type est signalée avec sa position.

Créer sig-app-1

$ scw instance server create \
    name=sig-app-1 \
    type=PRO2-XXS \
    image=ubuntu_noble \
    zone=fr-par-1 \
    cloud-init=@signalements.yaml \
    tags.0=app=signalements tags.1=env=prod \
    --wait -o json > sig-app-1.json
$ jq -r '.id, .state, .public_ips[0].address' sig-app-1.json

Chaque argument :

  • name=sig-app-1 : le nom, qui devient aussi le nom d'hôte de la machine ;
  • type=PRO2-XXS : le type d'instance choisi plus haut ;
  • image=ubuntu_noble : le libellé de l'image Ubuntu 24.04, résolu dans la zone visée ;
  • zone=fr-par-1 : explicite, comme convenu à la leçon 3 ;
  • cloud-init=@signalements.yaml : les données utilisateur, lues dans le fichier ;
  • tags.N=... : des étiquettes libres, qui serviront à retrouver et à facturer les ressources de l'application (leçon 8) ;
  • --wait : attendre que l'instance soit dans un état stable (en marche) avant de rendre la main ;
  • -o json : une sortie exploitable par jq, que l'on garde dans un fichier.

Le volume racine est créé automatiquement en stockage bloc, et une IP flexible est attribuée (ip=new par défaut). Attention : --wait attend que la machine soit démarrée, pas que cloud-init ait fini. Il reste plusieurs minutes de mises à jour et d'installation.

Suivre cloud-init

Récupérez l'adresse et connectez-vous :

$ IP1=$(jq -r '.public_ips[0].address' sig-app-1.json)
$ ssh root@"$IP1" cloud-init status --wait --long

cloud-init status --wait bloque jusqu'à la fin de cloud-init, en affichant des points pendant l'attente, puis donne l'état final ; --long ajoute le détail (source de données détectée, erreurs éventuelles). Le code de sortie est ce qu'un script doit regarder : 0 si tout s'est bien passé, 1 si cloud-init a planté, 2 s'il a terminé avec des erreurs récupérables (un module qui a échoué, par exemple).

Si quelque chose ne va pas, trois journaux et une commande :

$ ssh root@"$IP1"
# less /var/log/cloud-init-output.log      # la sortie des commandes (apt, runcmd)
# less /var/log/cloud-init.log             # le journal détaillé de cloud-init lui-même
# cloud-init analyze blame                 # quels modules ont pris le plus de temps
# cloud-init query ds.meta_data.name       # une valeur des métadonnées, depuis le cache

Puis vérifiez l'application :

# systemctl status signalements
# journalctl -u signalements -n 20
# curl -s http://127.0.0.1:8000/sante

Depuis votre poste, un tunnel SSH donne accès à l'API sans l'exposer :

$ ssh -N -L 8000:127.0.0.1:8000 exploit@"$IP1" &
$ curl -s http://localhost:8000/sante
$ kill %1

-L 8000:127.0.0.1:8000 fait suivre le port 8000 de votre poste vers le port 8000 de l'interface locale de l'instance, à travers la connexion SSH chiffrée ; -N n'exécute aucune commande distante. Le fait que la connexion exploit@ fonctionne prouve au passage que le module users a bien fait son travail.

Interroger le service de métadonnées

Depuis l'instance, regardez ce qu'elle sait d'elle-même :

# scw-metadata NAME
# curl -s 'http://169.254.42.42/conf?format=json' | jq '{name, id, tags}'

scw-metadata est un petit outil des images Scaleway ; il lit le même service que curl. La réponse contient le nom, l'identifiant, les étiquettes, les adresses, les clés SSH publiques autorisées. Essayez maintenant de lire les données utilisateur, d'abord en tant qu'exploit, sans privilège :

$ curl -s http://169.254.42.42/user_data/cloud-init

La requête est refusée. Le code source de la source de données Scaleway de cloud-init l'explique : le service exige que la requête parte d'un port source inférieur à 1024, que seul root (ou un processus doté de la capacité CAP_NET_BIND_SERVICE) peut ouvrir. En root, curl peut choisir son port local :

# curl -s --local-port 1-1023 http://169.254.42.42/user_data/cloud-init

Cette fois, votre fichier signalements.yaml s'affiche, tel que vous l'avez envoyé. Retenez-le : les données utilisateur restent lisibles pendant toute la vie de l'instance, par tout processus root de la machine, et par quiconque peut consulter l'instance dans la console ou l'API.

Créer sig-app-2

La seconde instance est identique, dans l'autre zone. C'est tout l'intérêt de la description écrite : il n'y a rien à refaire, seulement à relancer.

$ scw instance server create \
    name=sig-app-2 type=PRO2-XXS image=ubuntu_noble zone=fr-par-2 \
    cloud-init=@signalements.yaml \
    tags.0=app=signalements tags.1=env=prod \
    --wait -o json > sig-app-2.json
$ ssh root@"$(jq -r '.public_ips[0].address' sig-app-2.json)" cloud-init status --wait

Vérifiez le résultat d'ensemble avec la commande de la leçon 3 :

$ scw instance server list zone=all tags=app=signalements -o json \
    | jq -r '.[] | [.name, .zone, .state, .commercial_type] | @tsv'

Remplacer plutôt que réparer

Une nouvelle version de Signalements, 1.3.0, est publiée. L'animal de compagnie se mettrait à jour par SSH. Le bétail se remplace :

  1. modifier l'étiquette de l'image dans signalements.yaml et la versionner ;
  2. créer sig-app-1b en fr-par-1 avec ce fichier, attendre cloud-init status --wait, vérifier /sante ;
  3. faire basculer le trafic (leçon 6 : retirer l'ancienne instance du répartiteur, ajouter la nouvelle) ;
  4. supprimer l'ancienne instance.

La même procédure sert à appliquer un correctif de sécurité, à changer de type d'instance ou à passer à une nouvelle version d'Ubuntu. Elle est plus lente qu'un apt upgrade sur place ; elle garantit en échange que ce qui tourne est exactement ce qui est décrit, et que l'on sait recréer la machine.

Arrêter et supprimer

Pour arrêter la facturation du calcul sans tout détruire, arrêtez par l'API :

$ scw instance server stop "$(jq -r .id sig-app-1.json)" zone=fr-par-1 --wait

Pour supprimer une instance avec son volume racine et son IP flexible :

$ scw instance server terminate "$(jq -r .id sig-app-1.json)" zone=fr-par-1 \
    with-ip=true with-block=true

Sans with-ip=true, l'IP flexible reste dans le projet, et reste facturée. with-block vaut prompt par défaut : la CLI demande confirmation avant de supprimer les volumes bloc. Faites de même pour sig-app-2 en fr-par-2.

Sous le capot

Les étapes de démarrage de cloud-init. cloud-init n'est pas un programme unique, mais une série de services systemd qui s'intercalent dans le démarrage de la machine. La documentation en décrit cinq étapes :

ÉtapeService systemdRôle
Détectiongénérateur systemd, ds-identifyReconnaître la plateforme (ici Scaleway) et activer cloud-init
Localcloud-init-local.serviceTrouver la source de données et préparer la configuration réseau, avant que le réseau ne monte
Réseaucloud-init.service (renommé cloud-init-network.service à partir de la version 24.3, sur les distributions récentes)Récupérer et traiter les données utilisateur, écrire les fichiers, créer les utilisateurs ; bloque l'ouverture de SSH
Configurationcloud-config.serviceModules qui n'affectent pas le reste du démarrage (configuration d'apt, runcmd...)
Finalecloud-final.serviceInstallation des paquets, scripts de l'utilisateur ; l'équivalent de l'ancien rc.local

Chaque module est rattaché à une étape par le fichier /etc/cloud/cloud.cfg de l'image. Dans la configuration par défaut de cloud-init, write_files et users_groups s'exécutent à l'étape réseau, runcmd à l'étape de configuration, et package_update_upgrade_install à l'étape finale. Un détail surprend : le module runcmd n'exécute rien. Il écrit vos commandes dans un script, que le module scripts_user lance à l'étape finale, après l'installation des paquets. C'est pour cela que notre systemctl enable --now signalements.service trouve Docker installé, alors que le fichier d'unité, lui, a été écrit bien avant que Docker n'existe. Si un fichier doit appartenir à un utilisateur créé par un paquet, l'option defer: true de write_files reporte son écriture à l'étape finale.

La source de données Scaleway. Le code de cloud-init (DataSourceScaleway.py) interroge http://169.254.42.42 puis http://[fd00:42::42], lit les métadonnées sur /conf, et les données utilisateur et fournisseur sur /user_data/cloud-init et /vendor_data/cloud-init. Pour ces deux dernières, il se lie volontairement à un port local inférieur à 1024, et réessaie avec le port suivant si celui-ci est occupé : c'est la protection vue plus haut, qui garantit que seul un processus privilégié lit les données utilisateur. cloud-init en garde ensuite une copie sur le disque, sous /var/lib/cloud/instance/, avec des droits 0600 (lisible par root seulement).

Comment cloud-init sait que ce n'est pas le premier démarrage. Il compare l'identifiant de l'instance, lu dans les métadonnées, à celui qu'il a enregistré sous /var/lib/cloud/. S'ils sont identiques, il saute les modules per-instance. C'est aussi pour cela qu'une image fabriquée à partir d'une instance configurée fonctionne : la nouvelle instance a un autre identifiant, cloud-init la traite comme neuve. Avant de fabriquer une image dorée, on nettoie l'état de cloud-init (cloud-init clean --logs --machine-id) pour que chaque copie reparte de zéro, avec son propre identifiant de machine.

Le temps volé. Sur une instance à vCPU partagés, l'hyperviseur peut retirer un cœur physique à votre vCPU pour le donner à un voisin. Le noyau invité le constate (l'hyperviseur le lui signale) et le compte comme du steal time. Une valeur durablement supérieure à quelques pour cent signifie que votre application attend le processeur : c'est un signal pour passer à des vCPU dédiés, pas pour ajouter des vCPU partagés.

Pièges courants

cloud-init status dit error, mais tout semble marcher. Un module a échoué (une faute de frappe dans un nom de paquet, une commande runcmd qui renvoie un code non nul) et les suivants ont continué. Le code de sortie 2 le signale ; /var/log/cloud-init-output.log donne la commande fautive. Traitez ce cas comme un échec : une instance « presque » configurée est une instance différente des autres.

Modifier les données utilisateur et redémarrer, sans effet. Les modules per-instance ne s'exécutent qu'une fois. La documentation de Scaleway le rappelle, et indique au passage que la CLI ne modifie pas les données utilisateur en place : scw instance server update <id> cloud-init=@fichier remplace tout le contenu. Le bon geste est de recréer l'instance.

Oublier la ligne #cloud-config. Sans elle, le fichier n'est pas reconnu comme un cloud-config, et rien de ce qu'il décrit ne s'applique. cloud-init schema le signale.

Un poweroff qui ne fait pas d'économie. Arrêter le système depuis l'intérieur met l'instance en veille, toujours facturée. Arrêtez par l'API ou la console.

L'IP flexible orpheline. Supprimer une instance sans with-ip=true laisse son IP dans le projet, facturée sans servir. La leçon 8 montre comment repérer ces ressources oubliées.

--wait n'attend pas cloud-init. Un script qui crée l'instance puis s'y connecte immédiatement pour tester l'application échoue au hasard. Enchaînez toujours avec cloud-init status --wait.

Un port Docker publié sur toutes les interfaces. -p 8000:8000 équivaut à -p 0.0.0.0:8000:8000. Docker ouvre lui-même le port dans le pare-feu du noyau, en contournant au passage les règles d'un outil comme ufw sur la machine. Sur une instance qui a une IP publique, l'application est alors exposée à Internet, quels que soient vos réglages locaux ; seul le groupe de sécurité du fournisseur (leçon 6) l'en protège.

Le type en pénurie au moment de redémarrer. Une instance arrêtée par l'API a rendu son hyperviseur. Si le type est en pénurie dans la zone au moment de la relancer, le démarrage peut échouer. Pour une instance qui doit pouvoir redémarrer à tout moment, la veille (facturée) ou une instance de rechange en marche sont des assurances.

Sécurité

Aucun secret dans les données utilisateur. C'est la règle la plus importante de la leçon. Les données utilisateur sont stockées par le fournisseur, affichées dans la console, renvoyées par l'API à toute identité qui peut lire l'instance, copiées sur le disque de la machine, et servies par le service de métadonnées pendant toute la vie de l'instance. Un mot de passe de base de données, une clé d'API ou un jeton de registre qui y figure est un secret partagé avec toutes ces personnes et tous ces processus. Fournissez les secrets par un canal fait pour cela : un gestionnaire de secrets (Scaleway Secret Manager, que Lyneko utilise pour ses applications), que l'instance interroge au démarrage avec une identité aux droits limités.

Le service de métadonnées est une cible. Toute application qui peut être amenée à faire une requête HTTP vers une adresse choisie par un attaquant (une falsification de requête côté serveur, ou SSRF : un champ « URL de l'avatar », un service de prévisualisation de liens, un proxy mal configuré) peut être retournée contre 169.254.x.x. Chez AWS, le service de métadonnées fournit en plus des identifiants temporaires pour le rôle IAM attaché à l'instance : une SSRF suffit alors à voler des droits sur le compte cloud. C'est ce qui a conduit AWS à créer IMDSv2 en 2019 : il faut d'abord obtenir un jeton de session par une requête PUT (avec un en-tête X-aws-ec2-metadata-token-ttl-seconds), puis le présenter dans chaque requête (X-aws-ec2-metadata-token). Une SSRF ordinaire ne sait faire que des GET sans en-têtes choisis ; AWS rejette en outre les PUT qui portent un en-tête X-Forwarded-For (signe d'un proxy), et la réponse au PUT porte par défaut une limite de sauts IP (TTL) de 1, ce qui l'empêche d'atteindre un équipement situé au-delà de l'instance. Configurez les instances AWS pour n'accepter que IMDSv2. Chez Scaleway, la protection des données utilisateur par port privilégié vise le même risque côté processus locaux ; les métadonnées de /conf, elles, restent lisibles par tout processus.

Bloquer l'accès aux métadonnées depuis les conteneurs. Un conteneur partage la pile réseau de sa machine (ou passe par elle) : il peut joindre 169.254.42.42. Si une application n'en a pas besoin, une règle de pare-feu sur l'instance (ou une politique réseau dans Kubernetes) qui interdit cette destination aux conteneurs ferme la porte.

SSH : des clés, des personnes, des traces. Pas de mot de passe (les images Scaleway n'en ont pas pour root), une clé par personne plutôt qu'une clé d'équipe, des comptes nominatifs pour l'administration quotidienne, et la révocation d'une clé dans l'IAM quand quelqu'un quitte l'équipe (en se souvenant qu'elle ne disparaît des instances qu'au redémarrage suivant : pour une révocation immédiate, on remplace les instances). À la leçon 6, SSH ne sera plus joignable depuis Internet, seulement par un bastion.

Les mises à jour de sécurité. package_upgrade: true met l'instance à jour à sa naissance. Ensuite, les images Ubuntu activent par défaut unattended-upgrades, qui installe automatiquement les mises à jour de sécurité ; certaines exigent un redémarrage (noyau), qui n'est pas automatique. Dans un modèle immuable, on préfère reconstruire régulièrement les instances à partir d'une image récente : c'est plus simple à raisonner que des machines qui se modifient seules.

Les étiquettes deviennent des métadonnées. Les tags d'une instance sont visibles par tout processus de la machine, via /conf. N'y mettez rien de confidentiel. Chez Scaleway, un tag de la forme AUTHORIZED_KEY=... sert même à injecter une clé SSH dans une instance précise : qui peut modifier les tags d'une instance peut donc s'y donner un accès.

En production

  • Des images dorées plutôt que des installations au démarrage. Notre cloud-config installe Docker et applique les mises à jour à chaque création : c'est lent (plusieurs minutes) et cela dépend des dépôts d'Ubuntu au moment de la création. En production, on fabrique une image avec tout le nécessaire déjà installé (cours Images de machines avec Packer), et cloud-init ne fait plus que la configuration propre à l'instance. Le démarrage passe de minutes à secondes, ce qui compte quand il faut remplacer des instances en urgence.
  • Des groupes plutôt que des instances. Créer sig-app-1 et sig-app-2 à la main, c'est déjà mieux que le vieux serveur, mais c'est encore deux animaux. Les groupes d'autoscaling maintiennent un nombre d'instances identiques à partir d'un modèle, en remplacent une qui échoue à sa vérification de santé, et en ajoutent quand la charge monte. Scaleway les propose depuis août 2026 en bêta publique dans les régions fr-par, nl-ams et pl-waw, avec des modèles d'instances (Instance templates) qui figent le type, l'image, le réseau et le cloud-init ; une bêta n'a pas de SLA, réservez-la aux environnements de test pour l'instant. Les équivalents éprouvés sont les Auto Scaling groups d'AWS, les Virtual Machine Scale Sets d'Azure et les managed instance groups de Google Cloud.
  • Dimensionner par la mesure. Le surdimensionnement « par sécurité » est la première source de gaspillage du cloud. Partez petit, mesurez (CPU, mémoire, temps volé, latence de l'application), et changez de type : c'est l'affaire d'une ligne quand l'instance se recrée.
  • Ou ne plus gérer d'instances du tout. Kubernetes (Kapsule chez Scaleway) prend en charge le placement, le redémarrage et le remplacement des conteneurs sur un ensemble de nœuds, qui sont eux-mêmes des instances gérées en groupe. C'est le choix de Lyneko pour ses propres applications, et l'objet des cours Kubernetes : les fondamentaux et Kapsule. Les instances restent le bon outil pour un service isolé, un bastion, ou un logiciel qui ne se conteneurise pas.
  • Tout ce qui est fait ici à la main sera du code. Le cours Terraform et OpenTofu : les fondamentaux décrit ces mêmes instances dans des fichiers versionnés, que l'on relit en demande de fusion avant de les appliquer.

Exercices

1. Lire une facture en puissance (niveau 100). Une instance de développement est utilisée de 9 h à 18 h, du lundi au vendredi. Le soir, la développeuse tape sudo poweroff en partant. Que paie l'équipe la nuit et le week-end ? Que proposez-vous ?

Solution

poweroff depuis le système met l'instance en veille : elle reste allouée sur son hyperviseur et facturée comme une instance en marche, plus son stockage et son IP. L'équipe paie donc 24 heures sur 24. Il faut l'arrêter par l'API (scw instance server stop), ce qui ne laisse que le stockage et l'IP flexible facturés, idéalement par une tâche planifiée le soir et un démarrage le matin. Mieux encore pour un environnement de développement : la supprimer le soir et la recréer le matin à partir de son cloud-config, ce qui ne laisse plus rien de facturé et vérifie chaque jour que la recréation fonctionne.

2. Déboguer un cloud-config (niveau 200). Une collègue vous montre ce fichier. L'instance démarre, mais Signalements ne tourne pas. Trouvez les trois problèmes, dans l'ordre où vous les rencontreriez.

packages:
  - docker
runcmd:
  - docker run -d -p 8000:8000 ghcr.io/lyneko-formation/signalements:1.2.0
write_files:
  - path: /etc/signalements/env
    content: DATABASE_URL=postgresql://sig:S3cret!@10.0.0.5/signalements
Solution
  1. La ligne #cloud-config manque en tête : le fichier n'est pas reconnu comme un cloud-config et rien de ce qu'il décrit n'est appliqué ; /var/log/cloud-init.log contient seulement un avertissement sur des données utilisateur non prises en charge. cloud-init schema l'aurait signalé avant la création.
  2. Une fois la ligne ajoutée, l'installation échoue : sous Ubuntu, Docker s'installe avec le paquet docker.io ; le nom docker ne désigne pas Docker. Le module de paquets échoue, la commande docker run de runcmd aussi (commande introuvable), et cloud-init status sort avec le code 2 ; /var/log/cloud-init-output.log montre les deux erreurs.
  3. Le mot de passe de la base est en clair dans les données utilisateur : lisible dans la console, par l'API et par le service de métadonnées. Même corrigé, ce fichier ne doit pas être utilisé ; le secret doit être changé, puisqu'il a déjà été exposé.

Deux remarques de plus : -p 8000:8000 expose l'API sur l'IP publique, et un conteneur lancé par docker run -d sans politique de redémarrage ne revient pas après un redémarrage de la machine.

3. Prouver l'immuabilité (niveau 200). Écrivez un petit script qui, pour une nouvelle version X.Y.Z de Signalements, crée une instance sig-app-1-X-Y-Z en fr-par-1 à partir d'un cloud-config où la version est remplacée, attend la fin de cloud-init, vérifie /sante par SSH, et n'affiche « prête » qu'en cas de succès. Ne supprimez pas l'ancienne instance dans le script : pourquoi ?

Solution
#!/usr/bin/env bash
set -euo pipefail
version="$1"                                   # par exemple 1.3.0
nom="sig-app-1-${version//./-}"
sed "s/signalements:[0-9.]*/signalements:${version}/; s/APP_VERSION=.*/APP_VERSION=${version}/" \
  signalements.yaml > "cloud-init-${version}.yaml"

scw instance server create name="$nom" type=PRO2-XXS image=ubuntu_noble zone=fr-par-1 \
  cloud-init=@"cloud-init-${version}.yaml" tags.0=app=signalements tags.1=env=prod \
  --wait -o json > "$nom.json"
ip=$(jq -r '.public_ips[0].address' "$nom.json")

ssh -o StrictHostKeyChecking=accept-new root@"$ip" cloud-init status --wait
ssh root@"$ip" curl -fsS http://127.0.0.1:8000/sante
echo "$nom prête ($ip)"

set -e arrête le script au premier échec : cloud-init status --wait qui renvoie 1 ou 2, ou curl -f qui reçoit une erreur HTTP. L'ancienne instance n'est pas supprimée parce que la bascule du trafic (leçon 6) doit avoir lieu avant, et qu'en cas de problème découvert après la bascule, garder l'ancienne instance quelques heures permet un retour arrière immédiat. La suppression est une étape distincte, une fois la nouvelle version validée en production.

4. Choisir un type (niveau 200). Pour chacun de ces usages, proposez une famille d'instances Scaleway et justifiez par le caractère partagé ou dédié des vCPU : (a) un agent de CI qui compile pendant dix minutes par heure ; (b) une base PostgreSQL autogérée de production ; (c) le bastion SSH de la leçon 6.

Solution

(a) Des vCPU partagés (PLAY2, PRO2, BASIC3) conviennent si les durées de compilation peuvent varier ; si le temps de CI devient un goulet, des vCPU dédiés réguliers ou des instances de calcul (COMPUTE3) réduisent la variance, et l'on peut supprimer l'agent entre deux usages. (b) Des vCPU dédiés (STANDARD3, POP2), parce qu'une base est sensible à la latence et que le temps volé se traduit directement en requêtes lentes ; et souvent, plutôt qu'une base autogérée, une base managée (leçon 2). (c) La plus petite instance partagée possible : un bastion ne fait que relayer des connexions SSH.

Récapitulatif

  • Une instance est une machine virtuelle louée, découpée par un hyperviseur dans un serveur physique partagé. Son type fixe vCPU, mémoire, stockage et bande passante.
  • Les vCPU partagés sont moins chers mais sujets au temps volé ; les vCPU dédiés donnent des performances constantes. Les gammes récentes de Scaleway démarrent sur un volume bloc, attaché par le réseau.
  • Une image est le point de départ du volume racine ; les images publiques se désignent par un libellé (ubuntu_noble), les images personnalisées (images dorées) se fabriquent à partir d'une instance ou d'un instantané.
  • Chaque état a son coût : la veille reste facturée comme une instance en marche ; seul l'arrêt par l'API libère le calcul ; les IP flexibles et les volumes survivent à la suppression si on ne les supprime pas explicitement.
  • Le service de métadonnées (169.254.42.42 chez Scaleway, 169.254.169.254 ailleurs) dit à l'instance qui elle est et lui transmet les données utilisateur.
  • cloud-init applique un cloud-config au premier démarrage, en cinq étapes ; runcmd s'exécute après l'installation des paquets ; cloud-init schema valide, cloud-init status --wait attend et renvoie un code de sortie exploitable.
  • Aucun secret dans les données utilisateur : elles sont lisibles par l'API, la console et la machine pendant toute sa vie. Le service de métadonnées est une cible des attaques SSRF (IMDSv2 chez AWS).
  • On traite les serveurs comme du bétail, pas comme des animaux de compagnie : infrastructure immuable, on remplace plutôt que de réparer.

Pour aller plus loin

  • La documentation de cloud-init, en particulier la référence des modules (avec la fréquence d'exécution de chacun) et la page sur les étapes de démarrage.
  • L'article de Chad Fowler, Trash Your Servers and Burn Your Code (2013), et celui de Randy Bias sur l'histoire de la formule pets vs cattle (2016).
  • L'article du blog sécurité d'AWS sur IMDSv2, Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities, pour comprendre chaque choix de conception du jeton de session.
  • La leçon suivante, Le stockage : bloc, fichier, objet, où Signalements gagne un volume de données et un bucket pour ses pièces jointes.
Voir ma constellation →

Sources