Aller au contenu
Images personnalisées et autoscaling

Images personnalisées et autoscaling

300 Concevoir ⏱ 1 h 30 cloudscalewaycloud-initdocker

À la fin, vous saurez

  • Fabriquer une image dorée propre, avec cloud-init réinitialisé, et la nommer pour pouvoir la retrouver
  • Rendre une image disponible dans une autre zone par l'export et l'import d'un instantané
  • Décrire une instance dans un modèle immuable et y séparer ce qui est cuit dans l'image de ce qui est donné au démarrage
  • Créer un groupe d'autoscaling relié à un répartiteur de charge, avec une cible d'utilisation et l'autoréparation
  • Remplacer progressivement les instances d'un groupe lors d'une nouvelle version
  • Expliquer les limites d'un groupe zonal en bêta, et concevoir une architecture qui les contourne

Prérequis

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

Pourquoi

Le lendemain d'un orage, les signalements d'arbres tombés et de chaussées inondées arrivent par centaines en une matinée. Les deux instances sig-app-1 et sig-app-2 du cours précédent saturent vers 9 h ; à 14 h, la charge est retombée à son niveau habituel. Ajouter deux instances à la main le matin, puis les retirer l'après-midi, suppose que quelqu'un surveille, qu'il sache créer une instance identique aux autres, et qu'il s'en souvienne en fin de journée.

La leçon 4 du cours précédent a posé les deux idées qui permettent d'automatiser ce geste : les serveurs se traitent comme du bétail, recréés à l'identique depuis une description, et leur configuration se fait au premier démarrage par cloud-init. Elle en a aussi montré la limite : notre cloud-init installe Docker, applique les mises à jour et télécharge l'image de l'application à chaque création, soit plusieurs minutes pendant lesquelles l'instance ne sert à rien. Pour réagir à un pic, il faut des instances qui démarrent vite et toutes pareilles. D'où les deux sujets de cette leçon :

  • l'image dorée, qui contient déjà tout ce qui ne change pas d'une instance à l'autre ;
  • le groupe d'autoscaling, qui maintient un nombre d'instances fabriquées depuis cette image, l'adapte à la charge, et remplace celles qui tombent en panne.

Le second est en bêta publique chez Scaleway depuis le 25 août 2026 : la leçon dit ce qui est documenté, ce qui ne l'est pas, et comment s'en servir sans en dépendre aveuglément.

Les concepts

Ce qui va dans l'image, ce qui va au démarrage

Une instance de Signalements est faite de couches qui ne changent pas au même rythme :

CoucheChangeOù la mettre
Système Ubuntu, correctifsChaque semaineImage, refabriquée régulièrement
Docker, outils d'exploitation, agent de supervisionChaque moisImage
Image de conteneur de SignalementsÀ chaque versionImage (préchargée), ou téléchargée au démarrage
Version à lancer, paramètres non secretsÀ chaque déploiementDonnées utilisateur du modèle
Secrets (DATABASE_URL)Rarement, mais sensiblesJamais dans l'image ni dans les données utilisateur : Secret Manager (leçon 8)
Identité de l'instance (nom, adresse, clés d'hôte SSH)Unique à chaque instanceGénérée au premier démarrage

La règle : cuire dans l'image ce qui est lent et commun, donner au démarrage ce qui varie, ne jamais cuire ce qui est secret ou unique. Une image qui contient une clé d'hôte SSH, un identifiant de machine ou un mot de passe le copie dans chaque instance qui en naît.

Une image, des instantanés, une zone

Chez Scaleway, une image personnalisée n'est pas un fichier : c'est un objet qui référence des instantanés de volumes, un pour le volume racine et éventuellement d'autres. Deux façons de la fabriquer :

  • scw instance server backup crée une image d'une instance, tous volumes compris ;
  • scw instance image create crée une image à partir d'instantanés choisis, ce qui permet de n'y mettre que le volume système.

Deux conséquences documentées :

  • créer une image est gratuit, mais ses instantanés sont facturés au stockage, et supprimer l'image ne supprime pas ses instantanés (scw instance image delete accepte with-snapshots=true pour le faire) ;
  • une image, comme ses instantanés, est zonale : celle fabriquée en fr-par-1 n'existe pas en fr-par-2. Pour la porter, on exporte l'instantané au format QCOW2 dans un bucket de la même région, puis on l'importe dans l'autre zone. La documentation impose une taille de volume entre 1 Go et 1 To, et ne prend pas en charge les buckets chiffrés en SSE-KMS pour l'export.

Le modèle d'instance

Un modèle d'instance (Instance template) décrit tout ce qu'il faut pour créer une instance : type, volumes (et l'instantané ou l'image de départ), groupe de sécurité, groupe de placement, réseaux privés, nombre d'adresses publiques, étiquettes, et données cloud-init. La documentation insiste : un modèle est immuable, on ne le modifie pas, on en crée un nouveau. C'est la propriété qui rend les groupes fiables : un modèle désigne une configuration précise, comme une étiquette d'image de conteneur désigne un contenu précis. La CLI 2.62 expose néanmoins scw instance template set-cloud-init et set-user-data : les données utilisateur semblent pouvoir être changées après coup, ce que la page de documentation des modèles ne dit pas. Dans le doute, traitez tout le modèle comme immuable, données utilisateur comprises : un changement, un nouveau modèle, un nouveau nom.

Les modèles sont exposés par l'API Instance en version v2alpha1, et scw instance template check valide qu'un modèle est utilisable avant de s'en servir.

Le groupe d'autoscaling

Un groupe d'autoscaling (Autoscaling group) maintient un ensemble d'instances identiques, créées depuis un modèle. Il fonctionne dans l'un de deux modes :

  • taille fixe : le groupe garde toujours le même nombre d'instances, de 1 à 50, et en recrée une si elle disparaît ;
  • autoscaling : le groupe fait varier le nombre d'instances entre un minimum et un maximum (de 1 à 50), pour maintenir une utilisation cible du processeur ou de la mémoire.

La cible n'est pas un seuil. La documentation le souligne : le groupe calcule le nombre d'instances nécessaire pour que l'utilisation moyenne soit proche de la cible, et n'agit que si ce nombre change. Un groupe peut donc rester stable à 82 % avec une cible de 80 %. C'est le modèle dit de suivi de cible (target tracking), le même que les politiques du même nom chez AWS : on ne dit pas « ajoute une instance au-dessus de 80 % », on dit « vise 60 % », et le système en déduit le reste. Trois réglages complètent la cible :

  • le pas d'ajout et de retrait (scale-out-step, scale-in-step) ;
  • les délais de stabilisation (cooldown) après un ajout et après un retrait, pendant lesquels le groupe n'agit plus, le temps que les nouvelles instances démarrent et que la mesure reflète leur présence ;
  • les métriques, collectées par Cockpit, l'observabilité de Scaleway (leçon 14).

Attaché à un répartiteur de charge, le groupe inscrit et retire lui-même ses instances dans un backend, et peut activer l'autoréparation (autohealing) : une instance qui échoue à la vérification de santé du répartiteur est remplacée, après un délai de grâce qui laisse aux nouvelles instances le temps de démarrer.

Ce que dit la bêta, au 5 octobre 2026

Les pages de Scaleway, toutes datées de juillet et août 2026, dessinent un produit utile mais jeune. Ce qu'il faut savoir avant de s'en servir :

PointCe que documente ScalewayConséquence
StatutBêta publique depuis le 25 août 2026, régions fr-par, nl-ams, pl-wawPas d'engagement de disponibilité ; l'API est en v1alpha2 et peut changer
PortéeUn groupe vit dans une zoneIl ne répartit pas lui-même sur plusieurs zones
CLI 2.62scw autoscaling n'accepte que fr-par-1, fr-par-2, fr-par-3Ailleurs, passer par l'API ou attendre une version plus récente de la CLI
RépartiteurSeuls les répartiteurs de la même zone peuvent être attachésUn répartiteur en fr-par-1 ne sert pas un groupe en fr-par-2
MétriquesProcesseur et mémoire ; le SDK v1alpha2 n'accepte qu'une cible par groupe, la console en annonce plusieursPas de mise à l'échelle sur la latence ou la longueur d'une file
GPUPossible par l'API, mais sans effet de mise à l'échelle utileHors sujet pour Signalements
SuppressionSupprimer le groupe supprime ses instances, mais ni leurs adresses, ni leurs volumes, ni le répartiteurMénage à faire soi-même
ErreursType d'instance en rupture de stock, quota d'instances atteint, capacité du répartiteur dépasséeÀ surveiller ; la leçon 1 a montré que les quotas par défaut sont bas

En pratique

On fabrique l'image sig-app-2026-10-05-1 en fr-par-1, on en fait un modèle, puis un groupe qui tourne entre deux et six instances derrière lb-signalements. Les commandes ont été vérifiées avec l'aide de scw 2.62 et le SDK Go ; elles s'exécutent dans le projet signalements-prod (profil sig-prod de la leçon 1). Elles créent des ressources facturées : la dernière section les supprime.

Fabriquer l'image dorée

On part d'une instance de référence temporaire, créée avec un cloud-config de fabrication : celui de la leçon 4 du cours précédent, allégé de tout ce qui est propre à une instance. Fichier fabrication.yaml :

#cloud-config
package_update: true
package_upgrade: true
packages:
  - docker.io
  - jq
runcmd:
  - [systemctl, enable, docker]
  - [docker, pull, "ghcr.io/lyneko-formation/signalements:1.2.0"]

L'image de conteneur est préchargée : une instance née de cette image démarrera Signalements sans attendre un téléchargement. L'unité systemd, elle, viendra au démarrage, avec la version à lancer.

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

Puis on nettoie l'instance de tout ce qui doit être unique, et on l'éteint :

$ ssh root@"$IP" 'cloud-init clean --logs --machine-id && rm -f /etc/ssh/ssh_host_* && shutdown -h now'
  • cloud-init clean supprime l'état de cloud-init sous /var/lib/cloud : au prochain démarrage, cloud-init se croira sur une nouvelle instance et appliquera les données utilisateur du modèle. Sans ce nettoyage, il reconnaîtrait souvent sa propre trace et sauterait une partie du travail (leçon 4 du cours précédent, Sous le capot).
  • --logs efface les journaux de fabrication, qui n'ont rien à faire dans l'image.
  • --machine-id remet /etc/machine-id à l'état « non initialisé » ; la documentation de cloud-init le présente comme la bonne pratique pour une image dorée, afin que chaque instance génère un identifiant unique, utilisé notamment par systemd et journald.
  • Les clés d'hôte SSH sont supprimées : Ubuntu les régénère au premier démarrage. Sans cela, toutes les instances partageraient la même identité SSH, et la compromission d'une seule permettrait d'usurper toutes les autres.

On attend l'arrêt, on fabrique l'image à partir du seul volume système, puis on supprime l'instance de référence :

$ SRV=$(jq -r .id fabrication.json)
$ scw instance server wait "$SRV" zone=fr-par-1
$ VOL=$(scw instance server get "$SRV" zone=fr-par-1 -o json | jq -r '.volumes["0"].id')
$ SNAP=$(scw block snapshot create volume-id="$VOL" name=sig-app-2026-10-05-1 \
    zone=fr-par-1 -o json | jq -r .id)
$ scw block snapshot wait "$SNAP" zone=fr-par-1
$ IMG=$(scw instance image create name=sig-app-2026-10-05-1 snapshot-id="$SNAP" arch=x86_64 \
    tags.0=app=signalements tags.1=base=ubuntu_noble tags.2=signalements=1.2.0 \
    zone=fr-par-1 -o json | jq -r '.image.id // .id')
$ scw instance server terminate "$SRV" zone=fr-par-1 with-ip=true with-block=true
  • Le nom porte la date et un numéro : on fabriquera une image par semaine, et l'on doit pouvoir dire laquelle tourne.
  • Les étiquettes disent ce que contient l'image : distribution de départ, version de l'application préchargée. Elles servent à retrouver les images et à les nettoyer.
  • arch=x86_64 doit correspondre au type d'instance (PRO2 est x86 ; les gammes ARM demanderaient arm64).
  • Le filtre jq .image.id // .id tolère les deux formes de réponse, enveloppée ou non ; vérifiez celle de votre version de la CLI.

Porter l'image en fr-par-2

Pour un second groupe dans l'autre zone (voir En production), l'image doit exister en fr-par-2. On exporte l'instantané dans un bucket de la région, puis on l'importe :

$ scw block snapshot export-to-object-storage "$SNAP" bucket=signalements-images-<suffixe> \
    key=sig-app-2026-10-05-1.qcow2 zone=fr-par-1
$ SNAP2=$(scw block snapshot import-from-object-storage bucket=signalements-images-<suffixe> \
    key=sig-app-2026-10-05-1.qcow2 name=sig-app-2026-10-05-1 zone=fr-par-2 -o json | jq -r .id)
$ scw block snapshot wait "$SNAP2" zone=fr-par-2
$ scw instance image create name=sig-app-2026-10-05-1 snapshot-id="$SNAP2" arch=x86_64 \
    tags.0=app=signalements zone=fr-par-2

L'export prend du temps, proportionnel à la taille du volume : attendez que l'objet soit complet dans le bucket (état visible dans la console) avant de lancer l'import. Le bucket ne doit pas être chiffré en SSE-KMS. Une fois l'import terminé, l'objet QCOW2 peut être supprimé, ou gardé comme copie hors zone de l'image.

Écrire le modèle

Le modèle dit tout de l'instance, sauf ce que le groupe ajoute lui-même. On récupère d'abord l'instantané qui porte le volume racine de l'image, puis on crée le modèle sans adresse publique, sur le réseau privé :

$ PN=$(scw vpc private-network list name=pn-signalements region=fr-par -o json \
    | jq -r '.[] | select(.name == "pn-signalements") | .id')
$ SG=$(scw instance security-group list name=sg-sig-app zone=fr-par-1 -o json \
    | jq -r '.[] | select(.name == "sg-sig-app") | .id')
$ TPL=$(scw instance template create name=sig-app-2026-10-05-1 \
    server-type=PRO2-XXS \
    volumes.0.volume-type=sbs volumes.0.name=racine volumes.0.base-snapshot-id="$SNAP" \
    volumes.0.perf-iops=5000 \
    private-networks.0.private-network-id="$PN" \
    security-group-id="$SG" \
    public-ipv4-count=0 public-ipv6-count=0 \
    server-tags.0=app=signalements server-tags.1=image=sig-app-2026-10-05-1 \
    zone=fr-par-1 -o json | jq -r '.template.id // .id')
  • volumes.0.base-snapshot-id : le volume racine de chaque instance part de l'instantané de l'image dorée.
  • public-ipv4-count=0 : les instances du groupe n'ont pas d'adresse publique ; elles sortent par la passerelle (leçon 6 du cours précédent) et sont jointes par le répartiteur sur le réseau privé.
  • server-tags.N : les étiquettes que porteront les instances créées depuis le modèle, distinctes des étiquettes du modèle lui-même (tags.N).

Puis les données cloud-init, propres à ce déploiement : la version à lancer, et l'unité systemd. Fichier demarrage.yaml :

#cloud-config
write_files:
  - path: /etc/signalements/env
    permissions: "0640"
    content: |
      APP_VERSION=1.2.0
  - path: /usr/local/sbin/signalements-ip
    permissions: "0755"
    content: |
      #!/bin/sh
      # Écrit l'adresse de l'interface privée (172.16.20.0/22) pour l'unité systemd.
      ip=$(ip -4 -o addr show | awk '$4 ~ /^172\.16\.2[0-3]\./ {split($4, a, "/"); print a[1]; exit}')
      [ -n "$ip" ] || exit 1
      echo "IP_PRIVEE=$ip" > /run/signalements.ip
  - 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]
      EnvironmentFile=-/run/signalements.ip
      ExecStartPre=/usr/local/sbin/signalements-ip
      ExecStartPre=-/usr/bin/docker rm -f signalements
      ExecStart=/usr/bin/docker run --rm --name signalements --env-file /etc/signalements/env -p ${IP_PRIVEE}:8000:8000 ghcr.io/lyneko-formation/signalements:1.2.0
      Restart=always
      RestartSec=5

      [Install]
      WantedBy=multi-user.target
runcmd:
  - [systemctl, daemon-reload]
  - [systemctl, enable, --now, signalements.service]

C'est la version durable de la retouche faite à la main dans la leçon 6 du cours précédent : chaque instance calcule au démarrage l'adresse de son interface privée, celle qui appartient au bloc 172.16.20.0/22, et n'expose le port 8000 que sur elle. Quelques détails de systemd, tirés de systemd.exec(5) et systemd.service(5) :

  • le calcul est fait par un petit script plutôt que dans l'unité : systemd interprète lui-même les $ et les barres obliques inverses de ses lignes de commande, et une expression shell un peu complexe y devient vite illisible ;
  • le fichier d'environnement est lu juste avant le lancement de chaque commande ; le tiret de EnvironmentFile=- le rend facultatif, puisqu'il n'existe pas encore quand le premier ExecStartPre s'exécute ;
  • ${IP_PRIVEE} est remplacé par systemd dans la ligne ExecStart, sans passer par un shell ;
  • si le script ne trouve pas d'adresse privée, il échoue, et le service ne démarre pas sur une mauvaise interface : mieux vaut une instance que le répartiteur déclare malade qu'un port exposé au mauvais endroit.

La DATABASE_URL n'y figure pas : elle viendra de Secret Manager (leçon 8).

$ scw instance template set-cloud-init "$TPL" content=@demarrage.yaml zone=fr-par-1
$ scw instance template check "$TPL" zone=fr-par-1
$ scw instance template create-server "$TPL" zone=fr-par-1

La dernière commande crée une instance depuis le modèle : c'est le test. Connectez-vous par le bastion, vérifiez cloud-init status --wait, systemctl status signalements et curl http://<ip privée>:8000/sante, puis supprimez-la. Un modèle qui n'a jamais produit une instance saine ne doit pas alimenter un groupe.

Préparer un backend pour le groupe

Le groupe s'attache à un backend d'un répartiteur de sa zone, et le tient à jour lui-même. On ne lui donne pas le backend existant de lb-signalements, qui contient les adresses de sig-app-1 et sig-app-2 : la documentation dit que le groupe maintient le backend à jour, sans préciser ce qu'il fait des serveurs qu'il n'a pas créés. On crée donc un second backend, vide. La documentation de l'API le fait avec une liste server_ip vide ; l'aide de la CLI 2.62 présente server-ip comme obligatoire. On suit l'exemple documenté, par l'API :

$ LB=$(scw lb lb list name=lb-signalements zone=fr-par-1 -o json \
    | jq -r '.[] | select(.name == "lb-signalements") | .id')
$ curl -s -X POST -H @<(printf 'X-Auth-Token: %s' "$(scw config get secret-key)") \
    -H "Content-Type: application/json" \
    "https://api.scaleway.com/lb/v1/zones/fr-par-1/lbs/$LB/backends" \
    -d '{"name": "sig-asg", "forward_protocol": "http", "forward_port": 8000,
         "forward_port_algorithm": "roundrobin", "sticky_sessions": "none",
         "health_check": {"port": 8000, "check_delay": 5000, "check_timeout": 2000,
                          "check_max_retries": 3,
                          "http_config": {"uri": "/sante", "method": "GET", "code": 200}},
         "server_ip": []}' | jq -r .id

Notez l'identifiant renvoyé dans BE_ASG. L'en-tête d'authentification est lu dans un descripteur de fichier (-H @fichier, accepté par curl depuis sa version 7.55) plutôt que passé en argument : la clé secrète n'apparaît ainsi ni dans l'historique du shell ni dans la liste des processus. La vérification de santé est la même que celle du cours précédent : GET /sante doit répondre 200, ce qui teste aussi la base. Les délais sont exprimés en millisecondes dans l'API, alors que la CLI accepte 5s.

Créer le groupe

$ GRP=$(scw autoscaling group create name=sig-app-par1 template-id="$TPL" \
    scaling-policy-spec.minimum-size=2 \
    scaling-policy-spec.maximum-size=6 \
    scaling-policy-spec.cpu-target.target-avg-percent=60 \
    scaling-policy-spec.scale-out-step=2 \
    scaling-policy-spec.scale-in-step=1 \
    scaling-policy-spec.scale-out-cooldown=300s \
    scaling-policy-spec.scale-in-cooldown=900s \
    load-balancer-configuration-spec.load-balancer-id="$LB" \
    load-balancer-configuration-spec.backends.0.backend-id="$BE_ASG" \
    load-balancer-configuration-spec.backends.0.address-family=ipv4 \
    load-balancer-configuration-spec.backends.0.private-network-id="$PN" \
    load-balancer-configuration-spec.auto-healing.enabled=true \
    load-balancer-configuration-spec.auto-healing.grace-period=300s \
    tags.0=app=signalements zone=fr-par-1 -o json | jq -r .id)

Chaque réglage traduit une décision :

  • 2 à 6 instances : deux au repos pour supporter la perte d'une, six au plus, ce que permettent les quotas et la capacité du répartiteur (à vérifier, voir Pièges).
  • Cible de 60 % de processeur : assez basse pour garder de la marge pendant que de nouvelles instances démarrent, assez haute pour ne pas payer des machines inoccupées. Avec des vCPU partagés (PRO2), le processeur affiché peut être faussé par le temps volé (leçon 4 du cours précédent) : observez le comportement réel avant de fixer la cible.
  • Monter vite, descendre lentement : deux instances à la fois et un délai de cinq minutes en montée ; une à la fois et quinze minutes en descente. Un pic d'après orage arrive en quelques minutes ; mieux vaut garder des instances inutiles un quart d'heure de trop que d'osciller.
  • Autoréparation avec délai de grâce de cinq minutes : le temps qu'une instance démarre, que cloud-init s'exécute et que Signalements réponde ; un délai trop court ferait remplacer des instances saines qui n'ont pas fini de démarrer, en boucle.

Les durées s'écrivent au format Go (300s, 15m), comme toutes les durées de la CLI.

Suivez la création, puis basculez le trafic :

$ scw autoscaling servers list group-id="$GRP" zone=fr-par-1
$ scw autoscaling logs list group-id="$GRP" zone=fr-par-1
$ FE=$(scw lb frontend list lb-id="$LB" zone=fr-par-1 -o json | jq -r '.[] | select(.name == "http") | .id')
$ scw lb frontend update "$FE" backend-id="$BE_ASG" name=http inbound-port=80 zone=fr-par-1

Le dernier pas fait passer le frontend du backend des instances manuelles à celui du groupe. Gardez l'ancien backend et ses instances quelques jours : revenir en arrière tient en une commande.

Les journaux du groupe contiennent, selon la documentation, des lignes comme Scaling event: added X instance(s) ou Auto-healing: replacing unhealthy instance : ce sont elles qu'on lit après un pic, et elles s'affichent aussi dans un tableau de bord Grafana de Cockpit.

Déployer une nouvelle version

Signalements 1.3.0 est publiée. On ne touche pas aux instances : on fabrique l'image sig-app-2026-10-12-1, ou, si seule l'application change, on garde l'image et l'on écrit un nouveau demarrage.yaml avec la nouvelle version ; puis un nouveau modèle, qu'on teste avec create-server, et qu'on donne au groupe :

$ scw autoscaling group update "$GRP" template-id="$TPL2" zone=fr-par-1

Les nouvelles instances naissent du nouveau modèle. La documentation ne dit pas que le groupe remplace les instances existantes ; on organise donc le remplacement progressif :

  1. relever temporairement le minimum (par exemple de 2 à 4), pour que le groupe crée des instances neuves, du nouveau modèle ;
  2. attendre qu'elles soient saines dans le backend ;
  3. supprimer une à une les anciennes instances (scw instance server terminate), en vérifiant le service entre chaque suppression ;
  4. remettre le minimum à sa valeur.

C'est lent et manuel ; c'est aussi le comportement le plus sûr tant que le produit ne documente pas de remplacement automatique.

Nettoyer les images

Une image par semaine, ce sont des instantanés qui s'accumulent. On garde les trois dernières et celle qui tourne :

$ scw instance image list zone=fr-par-1 -o json \
    | jq -r '[.[] | select(.name | startswith("sig-app-"))] | sort_by(.creation_date) | .[:-3][] | [.id, .name] | @tsv'

La commande liste les images de Signalements, de la plus ancienne à la plus récente, sauf les trois dernières. Pour chacune, vérifiez qu'aucun modèle en service ne s'y réfère, puis scw instance image delete <id> with-snapshots=true zone=fr-par-1, sans oublier la copie en fr-par-2.

Démonter

$ scw lb frontend update "$FE" backend-id=<ancien backend> name=http inbound-port=80 zone=fr-par-1
$ scw autoscaling group delete "$GRP" zone=fr-par-1
$ scw instance template delete "$TPL" zone=fr-par-1

Supprimer le groupe supprime ses instances, mais pas leurs volumes ni le backend : vérifiez avec scw block volume list zone=fr-par-1 et l'inventaire de la leçon 8 du cours précédent.

Sous le capot

Le suivi de cible, en un calcul. Si six instances tournent à 90 % de processeur en moyenne et que la cible est 60 %, la charge totale vaut 6 × 0,9 = 5,4 « instances pleines » ; pour la ramener à 60 %, il faut 5,4 / 0,6 = 9 instances. Le groupe arrondit, borne par le maximum, applique son pas, puis attend la fin du délai de stabilisation avant de recalculer. Le calcul suppose que la charge se répartit uniformément et qu'elle est proportionnelle au nombre d'instances : c'est vrai pour une API sans état derrière un répartiteur en tourniquet, faux pour une application où chaque instance porte des sessions longues.

Pourquoi la mise à l'échelle arrive toujours en retard. Entre le début d'un pic et la première requête servie par une nouvelle instance s'écoulent : la collecte des métriques par Cockpit et leur agrégation, le délai de décision, la création de l'instance par le plan de contrôle, le démarrage du système, cloud-init, le démarrage du conteneur, puis les vérifications de santé du répartiteur (trois réussites espacées de cinq secondes dans notre réglage). L'image dorée raccourcit le milieu de cette chaîne, pas ses extrémités. C'est pourquoi le minimum reste à deux et la cible à 60 % : la marge absorbe le début du pic pendant que les renforts arrivent.

L'autoréparation dépend de la vérification de santé. Le groupe ne sait pas qu'une instance est malade : c'est le répartiteur qui le constate, par GET /sante, et le groupe qui agit. Une vérification trop superficielle (un simple test TCP) laisse en service une instance dont l'application répond des erreurs ; une vérification qui teste la base, comme la nôtre, peut au contraire faire remplacer toutes les instances quand la base est indisponible, alors qu'elles n'y sont pour rien. Le délai de grâce et le nombre d'échecs tolérés sont les seuls amortisseurs.

Un modèle, une image, un contenu. La chaîne image datée → modèle nommé comme l'image → instances étiquetées avec le nom de l'image permet de répondre, sur n'importe quelle instance, à la question « qu'est-ce qui tourne ici ? ». C'est la même discipline que l'épinglage des images de conteneur par empreinte vu dans le cours Construire des images de conteneurs.

Pièges courants

L'image qui garde le passé. Oublier cloud-init clean produit des instances où cloud-init ne rejoue pas les données du modèle ; oublier --machine-id donne à toutes les instances le même identifiant de machine, ce qui trouble journald, les agents de supervision et parfois le DHCP ; oublier les clés d'hôte SSH donne à toutes la même identité.

Une image dans la mauvaise zone. Le modèle en fr-par-2 ne trouve pas l'instantané de fr-par-1. Images, instantanés, modèles et groupes sont zonaux : on porte l'image avant d'écrire le modèle.

Un groupe qui ne peut pas grandir. Le maximum vaut 6, mais l'organisation n'a droit qu'à 5 PRO2-XXS (quota d'un compte à identité validée, leçon 1), dont 2 déjà utilisées par les instances manuelles. Le groupe affiche Insufficient Instance quota au premier pic. Même problème avec un type d'instance en rupture de stock dans la zone, ou un répartiteur trop petit pour le nombre de serveurs : les trois erreurs sont décrites dans la page Fix common Autoscaling group errors, et se préviennent en vérifiant quotas et capacité en concevant le groupe.

L'oscillation. Une cible trop proche de l'utilisation habituelle, un délai de descente trop court, et le groupe ajoute puis retire des instances toutes les dix minutes, chaque cycle coûtant au minimum une heure de facturation par instance (leçon 8 du cours précédent). Monter vite, descendre lentement.

L'autoréparation en cascade. La base de données tombe, /sante échoue partout, et le groupe remplace toutes les instances, qui échouent à leur tour : la panne de la base devient une tempête de créations. Gardez un délai de grâce généreux, et suivez les journaux du groupe pendant un incident de base.

Un modèle modifié en place. Changer les données utilisateur d'un modèle en service (si la CLI le permet) rend impossible de savoir avec quelle configuration chaque instance est née. Un changement, un nouveau modèle.

La suppression qui laisse des restes. Supprimer le groupe ne supprime ni volumes, ni adresses, ni répartiteur. Le ménage suit l'inventaire.

Sécurité

  • L'image est un livrable. Elle contient un système, des paquets et une image de conteneur : elle se scanne (le cours Construire des images de conteneurs présente Trivy, qui sait aussi analyser un système de fichiers) et se reconstruit chaque semaine, pour que les instances neuves ne naissent pas avec des failles corrigées depuis.
  • Aucun secret dans l'image ni dans le modèle. Les données utilisateur d'un modèle sont lisibles par l'API et, depuis l'instance, par le service de métadonnées (leçon 4 du cours précédent). Les secrets arrivent au démarrage par Secret Manager, avec une identité limitée (leçon 8).
  • Pas d'adresse publique sur les instances du groupe : elles sont jointes par le répartiteur sur le réseau privé, administrées par le bastion.
  • Des droits séparés. Fabriquer des images (InstancesImageCreate), modifier un modèle et modifier un groupe sont des actions sensibles : quiconque peut changer le modèle d'un groupe peut faire tourner n'importe quel code en production. Seuls le pipeline de fabrication et le groupe d'exploitation en ont besoin (leçon 1).
  • Le remplacement est une mesure de sécurité. Une instance soupçonnée de compromission se supprime : le groupe en recrée une saine depuis le modèle, et l'on garde, si nécessaire, un instantané de son disque pour l'analyse.

En production

Deux zones, deux groupes. Un groupe est zonal et ne s'attache qu'à un répartiteur de sa zone : un seul groupe en fr-par-1 remet tous les œufs dans le panier que la leçon 3 du cours précédent avait appris à éviter. Deux architectures tiennent :

  • un groupe et un répartiteur par zone, devant lesquels un service régional répartit le trafic : Edge Services (leçon 11) ou le DNS, avec ses limites de cache ;
  • un groupe dans une zone et une capacité fixe dans l'autre, inscrite à la main dans un second backend, et une redirection manuelle en cas de perte de la zone du groupe. Plus simple, mais la seconde zone ne profite pas de l'autoscaling.

Dans les deux cas, chaque zone doit pouvoir porter seule la charge habituelle : c'est la stabilité statique.

Fabriquer les images par un pipeline. Les commandes de cette leçon s'automatisent : un workflow hebdomadaire crée l'instance de référence, attend cloud-init, nettoie, fabrique l'image, la teste avec un modèle de test, la porte dans l'autre zone et publie son nom. Packer, l'outil de HashiCorp, fait précisément ce travail, avec un greffon maintenu par Scaleway (scaleway/packer-plugin-scaleway) ; le cours Images de machines avec Packer le détaille.

Ne pas dépendre de la bêta pour survivre. Tant que le produit est en bêta, le groupe est une optimisation : il absorbe les pics et répare les pannes simples. Le minimum, lui, doit suffire à la charge habituelle, et l'équipe doit savoir créer une instance depuis le modèle à la main (create-server) si le groupe ne répond plus. C'est la même logique que celle de la leçon 3 du cours précédent sur le plan de contrôle : une architecture qui a besoin de créer des ressources pour survivre dépend de la disponibilité de l'API.

Et Kubernetes ? Un groupe d'autoscaling d'instances est la réponse la plus simple à « plus de copies de la même application ». Quand l'équipe gère plusieurs applications, des déploiements fréquents et des ressources hétérogènes, un orchestrateur fait mieux : c'est ce que Lyneko utilise avec Kapsule (cours Kapsule : Kubernetes managé chez Scaleway). Pour une ou deux applications sur des instances, le groupe évite d'avoir à exploiter un cluster.

Exercices

1. Image ou démarrage (niveau 200). Pour chacun de ces éléments, dites s'il va dans l'image dorée, dans les données utilisateur du modèle, ou ailleurs, et pourquoi : (a) l'agent de collecte des journaux ; (b) le mot de passe de la base ; (c) la version 1.3.0 de Signalements à lancer ; (d) la clé publique SSH de l'équipe d'exploitation ; (e) le fuseau horaire Europe/Paris ; (f) le certificat TLS du répartiteur.

Solution

(a) Image : commun à toutes les instances, change rarement, lent à installer. (b) Ailleurs : Secret Manager, lu au démarrage avec une identité limitée ; jamais dans l'image ni dans les données utilisateur, lisibles par l'API et le service de métadonnées. (c) Données utilisateur du modèle, si l'image n'est pas refabriquée à chaque version ; sinon, image (préchargée) et données utilisateur (version lancée), ce qui accélère le démarrage. (d) Ni l'un ni l'autre de préférence : les clés du projet sont injectées par Scaleway au démarrage, ou passées par le bastion ; les cuire dans l'image impose de refabriquer l'image à chaque arrivée ou départ. (e) Image : commun et stable. (f) Ailleurs : le certificat vit sur le répartiteur, qui termine TLS ; les instances ne le voient pas.

2. Calcul de cible (niveau 300). Le groupe a une cible de 60 %, un minimum de 2, un maximum de 6, un pas de montée de 2. Il compte 3 instances à 95 % de processeur. Combien d'instances le calcul de suivi de cible demande-t-il, combien le groupe en ajoute-t-il au premier événement, et combien d'instances tournent après le délai de stabilisation si la charge reste la même ?

Solution

Charge totale : 3 × 0,95 = 2,85. Instances nécessaires pour 60 % : 2,85 / 0,6 = 4,75, arrondi à 5. Le groupe veut passer de 3 à 5, soit + 2, ce qui correspond au pas : il ajoute 2 instances. Après le délai, 5 instances portent 2,85, soit 57 % chacune : sous la cible, sans que le calcul ne demande une sixième instance. Le groupe reste à 5. Si le pas avait été de 1, il aurait fallu deux événements, séparés par le délai de stabilisation, pour atteindre 5 : d'où l'intérêt de monter par pas plus grands.

3. Remplacement progressif (niveau 300). Écrivez le script qui remplace les instances d'un groupe de taille 2 par celles d'un nouveau modèle, sans jamais descendre sous deux instances saines dans le backend. Utilisez scw autoscaling group update, scw autoscaling servers list et scw lb backend list-statistics.

Solution
#!/usr/bin/env bash
set -euo pipefail
GRP=$1 TPL2=$2 LB=$3 BE=$4 ZONE=fr-par-1
anciens=$(scw autoscaling servers list group-id="$GRP" zone="$ZONE" -o json | jq -r '.[].server_id')
scw autoscaling group update "$GRP" template-id="$TPL2" zone="$ZONE"
scw autoscaling group update "$GRP" scaling-policy-spec.minimum-size=4 zone="$ZONE"
sains() { scw lb backend list-statistics lb-id="$LB" zone="$ZONE" -o json \
  | jq --arg be "$BE" '[.[] | select(.backend_id == $be and .last_health_check_status == "passed")] | length'; }
until [ "$(sains)" -ge 4 ]; do sleep 30; done
for id in $anciens; do
  scw instance server terminate "$id" with-ip=true with-block=true zone="$ZONE"
  sleep 60
  until [ "$(sains)" -ge 3 ]; do sleep 30; done
done
scw autoscaling group update "$GRP" scaling-policy-spec.minimum-size=2 zone="$ZONE"

Le script prend en arguments le groupe, le nouveau modèle, le répartiteur et le backend du groupe. Il relève le minimum pour obtenir deux instances neuves, attend quatre serveurs sains dans ce backend (les statistiques d'un répartiteur couvrent tous ses backends), supprime les anciennes une à une en vérifiant qu'il en reste au moins trois sains, puis rétablit le minimum. Les champs utilisés viennent du SDK Go : last_health_check_status (valeurs passed, failed, neutral...) dans l'objet BackendServerStats du répartiteur, server_id dans l'objet Server de l'API Autoscaling ; vérifiez-les dans votre version avant de vous en servir. Un tel script reste fragile ; c'est un argument pour attendre un remplacement progressif documenté, ou pour un orchestrateur.

4. Concevoir pour deux zones (niveau 300). Dessinez l'architecture de Signalements avec autoscaling dans deux zones, en tenant compte des limites documentées. Quels composants ajoutez-vous, que coûte la solution par rapport à un seul groupe, et que se passe-t-il si fr-par-1 est perdue ?

Solution

Deux groupes (sig-app-par1, sig-app-par2), deux modèles (un par zone, chacun sur l'image de sa zone), deux répartiteurs (un par zone, chacun avec le backend de son groupe), et en amont un point d'entrée régional qui répartit entre les deux répartiteurs : Edge Services (leçon 11) ou deux enregistrements DNS. Coût supplémentaire : un répartiteur, le double des instances minimales (2 par zone au lieu de 2 au total, si chaque zone doit porter seule la charge habituelle), la copie des images, et la complexité des déploiements (deux modèles à changer). Perte de fr-par-1 : le point d'entrée cesse d'envoyer du trafic au répartiteur de fr-par-1 (vérification de santé de l'origine), sig-app-par2 voit sa charge doubler et grandit jusqu'à son maximum, à condition que les quotas le permettent et que le plan de contrôle de fr-par-2 réponde.

Récapitulatif

  • Une image dorée contient ce qui est lent et commun ; le démarrage apporte ce qui varie ; les secrets et les identités uniques ne vont dans aucun des deux.
  • Avant de fabriquer l'image : cloud-init clean --logs --machine-id et suppression des clés d'hôte SSH.
  • Une image référence des instantanés, elle est zonale ; on la porte dans une autre zone par export et import QCOW2 dans un bucket de la région. Supprimer une image ne supprime pas ses instantanés.
  • Le modèle d'instance décrit tout, se traite comme immuable, se teste avec create-server avant de servir.
  • Le groupe d'autoscaling vise une utilisation cible (suivi de cible), monte par pas avec des délais de stabilisation, s'attache à un backend de répartiteur de sa zone et remplace les instances malades (autoréparation, avec délai de grâce).
  • Au 5 octobre 2026 : bêta publique (depuis le 25 août), API v1alpha2, groupe zonal, CLI limitée à fr-par, processeur ou mémoire comme métriques.
  • Une nouvelle version se déploie par un nouveau modèle et un remplacement progressif organisé.
  • En production : un groupe par zone, des images fabriquées par un pipeline, et un minimum qui ne dépend pas de la bêta.

Pour aller plus loin

  • Les pages Create and manage Autoscaling groups et Getting started with the Autoscaling Groups API de Scaleway, à relire à chaque évolution du produit, et la référence de l'API Autoscaling.
  • La documentation des politiques de suivi de cible d'AWS, pour un produit plus ancien qui a rencontré tous les problèmes de réglage décrits ici.
  • La référence de la ligne de commande de cloud-init, sous-commande clean.
  • Le greffon Packer de Scaleway, et le cours Images de machines avec Packer.
  • La leçon 12, qui approfondit le répartiteur auquel le groupe s'attache, et la leçon 14, qui montre les métriques sur lesquelles il décide.
Voir ma constellation →

Sources